Policy engine
How egress, CLI, tool and secret policy is written, compiled, distributed and enforced.
Policy is the layer between "this tier can do X" and "this session may do X". The tier sets the ceiling; policy sets the actual permission.
Composition
Rules:
- Deny always wins. Any layer's deny beats every layer's allow.
- Session overrides can only tighten. A session cannot grant itself a destination the project forbids.
- The platform floor is absolute. No customer configuration relaxes it.
- Compilation happens once, at admission. Enforcement needs no control plane, which is why it survives a control-plane outage.
The four policy domains
id: pol_build_tools
version: 7
egress:
mode: allowlist
allow:
- host: "registry.npmjs.org"
ports: [443]
- host: "*.githubusercontent.com"
ports: [443]
- host: "api.customer.com"
ports: [443]
deny:
- host: "*.pastebin.com"
dns:
resolvers: platform
block_dns_over_https: true
tls:
inspect: false
require_sni_match: true
limits:
bandwidth_mbps: 50
max_bytes_per_session_gb: 5
cli_allowlist:
mode: allowlist
binaries: [git, node, pnpm, python3, pytest, make, tar]
deny_always: [curl, wget, ssh, nc, socat]
tool_allowlist:
mode: allowlist
allow: ["crm.*", "search.web", "ticket.*"]
deny: ["crm.contact.delete"]
require_confirmation: ["email.send"]
secret_scope:
allow:
- ref: sec_github_token
tiers: [2]
max_ttl_s: 900
- ref: sec_crm_api_key
tiers: [0, 1, 2]
max_ttl_s: 3600
Egress
The most important domain, because network reach is the exfiltration path.
Modes: none (no egress at all), allowlist, blocklist, open.
| Mode | Correct for |
|---|---|
none | Untrusted code evaluation. The right default for tier 1 |
allowlist | Production at tiers 2–4 |
blocklist | Development only. Not safe for attacker-influenced input |
open | Explicit opt-in at the organization level, still logged and metered |
Matching is by host (with wildcards), CIDR, port and protocol. Resolution is pinned: the IP the proxy resolved is the only IP the connection may reach, which closes DNS rebinding.
Detail: networking and egress.
The platform floor
169.254.0.0/16 link-local and cloud metadata endpoints fd00::/8, fe80::/10 link-local IPv6 platform-internal CIDRs *.internal.workspaces.build SMTP 25, 465, 587 anti-abuse; outbound mail goes through a tool known-malicious feed refreshed hourly
Enforced at the namespace, the proxy and the host firewall — three places, because one misconfiguration should not be enough.
CLI allowlist
Tiers 2 and above. Enforced in the guest by an exec interposer in wbagent, checked on every
execve.
cli_allowlist: mode: allowlist # allowlist | blocklist | open binaries: [git, node, pnpm] paths: ["/usr/bin/*", "/workspace/bin/*"] # where binaries may live deny_always: [curl, wget, ssh, nc]
An allowlist reduces accident and casual misuse. It is not a sandbox: a permitted interpreter
can do anything that interpreter can do, so allowing python3 allows arbitrary code. The microVM is
what contains a determined attacker; the allowlist is what stops an agent casually running curl to
a destination the egress policy would have allowed anyway.
Denials surface as command_not_allowed with the binary and the rule, and appear in the session
trace.
Tool allowlist
Tier 0 and above. Evaluated per invocation.
tool_allowlist: mode: allowlist allow: ["crm.*", "search.web"] deny: ["crm.contact.delete"] require_confirmation: ["email.send", "payment.*"]
require_confirmation returns 202 with a confirmation ID instead of executing, and waits for an
approval. This is human-in-the-loop implemented in the platform rather than in each customer's agent
loop, so it cannot be forgotten or bypassed by a different code path.
Designing a safe tool catalog
The tool surface is the tier 0 security boundary.
| Do | Do not |
|---|---|
| Typed, constrained arguments: an ID, an enum, a bounded string | A URL, a SQL string, a file path, a shell command |
Authorize inside the handler against end_user_id | Assume the caller checked |
| Tight timeouts, bounded result size | Unbounded data |
| Idempotent, or honour the idempotency key | Hope retries do not happen |
side_effects: destructive on anything irreversible | Leave side effects undeclared |
A run_query tool taking arbitrary SQL is tier 1 work with tier 0's boundary.
Secret scope
Which secrets may be injected, into which tiers, for how long.
secret_scope:
allow:
- ref: sec_github_token
tiers: [2] # not tier 1 — a token in a code sandbox is a token in model output
max_ttl_s: 900
sessions: { end_user_bound: true } # only for sessions tagged with an end user
end_user_bound is useful for resellers: a secret belonging to one customer can only be injected
into a session tagged with that customer.
Detail: secrets.
Writing policy
Start from deny
egress: { mode: none }
cli_allowlist: { mode: allowlist, binaries: [] }
tool_allowlist: { mode: allowlist, allow: [] }
Then add what fails. The alternative — starting open and removing what looks dangerous — produces a policy that permits everything nobody thought to remove.
Simulate before saving
curl -sS "$WB_BASE_URL/policies/pol_build_tools/simulate" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "checks": [
{ "type": "egress", "host": "registry.npmjs.org", "port": 443 },
{ "type": "cli", "binary": "curl" },
{ "type": "tool", "name": "email.send" }
]}'
{
"results": [
{ "type": "egress", "decision": "allow", "rule": "allow:host=registry.npmjs.org" },
{ "type": "cli", "decision": "deny", "rule": "deny_always:curl" },
{ "type": "tool", "decision": "confirm", "rule": "require_confirmation:email.send" }
]
}
Preview the impact
curl -sS "$WB_BASE_URL/policies/pol_build_tools/impact?version=8&window=7d" \ -H "Authorization: Bearer $WB_API_KEY"
{
"sessions_in_window": 8420,
"would_have_been_affected": 412,
"breakdown": [
{ "rule": "deny_always:curl", "sessions": 380, "example_session": "sess_3Kd9LqX2" },
{ "rule": "egress removed:pypi.org", "sessions": 32 }
]
}
Tightening a policy that would have broken 5% of last week's sessions is worth knowing before saving, not after.
Versioning and rollout
Policies are versioned, never edited in place.
new sessions pick up the new version immediately running sessions egress within 30 s; CLI at the next execve; tools at the next call rollback PATCH to a previous version; the same propagation applies
A policy change is an audited action. The audit entry carries the full diff.
Enforcement points
| Domain | Where | Latency added |
|---|---|---|
| Egress | egress-proxy, from the compiled artifact | < 1 ms |
| CLI | wbagent exec interposer | < 0.1 ms per execve |
| Tool | api-gateway, per invocation | < 1 ms |
| Secret | secret-agent, at session start | One-time |
| Residency | admission and placer, and the storage layer | At create |
None of these calls the control plane during enforcement. The compiled artifact travels with the session, which is what makes a control-plane outage survivable.
Violations
{
"ts": "2026-10-06T09:18:41.402Z",
"session_id": "sess_2kQ9xwR4",
"end_user_id": "eu_customer_4821",
"event": "policy.violation",
"domain": "egress",
"rule": "default_deny",
"attempted": { "host": "telemetry.vendor.io", "port": 443 },
"action": "denied"
}
| Action | When |
|---|---|
denied | The operation failed; the session continues. The normal case |
confirm_required | Waiting on human approval |
session_killed | A floor violation, or repeated violations past a threshold |
A sudden spike in denials for one session is the strongest available signal that an agent has been
prompt-injected. It fires an egress.denied_spike webhook and is surfaced in both consoles.