The capability ceiling β how ACT sandboxes third-party tools
Enforcing Least Privilege in WebAssembly Agent Components via Intersection Policies
Current Situation Analysis
The integration of third-party tools into AI agent workflows introduces a critical security boundary problem. When an agent orchestrator invokes an external binary, the system faces a dual risk: the tool may be malicious, or the agent may misuse a benign tool due to hallucination or prompt injection. Traditional execution models, such as global package installation (npm install -g), grant tools ambient access to the host environment, creating an unacceptable blast radius for automated systems.
A pervasive misunderstanding in this domain is the conflation of isolation with authorization. Developers often assume that running code in a WebAssembly (Wasm) runtime like wasmtime provides sufficient security. While wasmtime offers strong isolation via a JIT compiler, linear memory boundaries, and the absence of direct host syscalls, isolation alone does not enforce least privilege. A Wasm component can still request broad capabilities through the WebAssembly System Interface (WASI). If the host wires up unrestricted WASI imports, the component retains significant power despite the VM boundary.
The industry lacks a standardized mechanism to reconcile the component author's intent with the operator's security requirements. Without a mediation layer, operators must choose between blocking useful tools or granting excessive permissions. This gap necessitates a policy architecture that treats capabilities as negotiable constraints rather than binary grants, ensuring that neither the component author nor the runtime operator can unilaterally escalate privileges.
WOW Moment: Key Findings
The ACT policy architecture resolves the trust gap through an intersection model that computes the effective policy as the mathematical intersection of the component's declared ceiling and the operator's runtime floor. This approach fundamentally alters the security posture compared to traditional execution models.
| Execution Model | Blast Radius | Redirect Safety | DNS Leakage | Privilege Escalation Vector |
|---|---|---|---|---|
| Raw Host Execution | Full Host | None | Full | Component or Agent |
| Static Sandbox | Container/VM | None | Partial | Misconfigured Sandbox |
| ACT Intersection | Declared Scope | Per-Hop Validation | Pre-Connect Deny | None (Intersection enforced) |
Why this matters: The intersection model guarantees that the effective permissions are always a subset of both the author's declaration and the operator's grant. A permissive operator cannot accidentally grant access beyond what the component declared, and a malicious component cannot access resources outside the operator's allowlist. This creates a verifiable "capability ceiling" that eliminates privilege escalation vectors inherent in single-sided policy models.
Core Solution
The solution relies on a three-layer architecture where policy enforcement sits atop WASI capabilities, which in turn run on the wasmtime VM. The implementation requires declarative manifests, runtime policy computation, and specialized enforcement hooks for DNS, HTTP redirects, and filesystem traversal.
1. Declarative Capability Ceilings
Component authors must declare required capabilities in a manifest. This manifest acts as a hard ceiling; the runtime will never grant permissions exceeding these declarations. The manifest is validated at build time and embedded into the component binary.
Manifest Structure (agent-tool.toml):
[tool.metadata]
name = "data-processor"
version = "1.0.0"
# Filesystem capabilities
[tool.capabilities."wasi:filesystem"]
description = "Access to processing database."
[[tool.capabilities."wasi:filesystem".grants]]
path = "/var/db/processing/**"
mode = "read-write"
# HTTP capabilities
[tool.capabilities."wasi:http"]
description = "Fetch configuration from trusted registry."
[[tool.capabilities."wasi:http".grants]]
host = "config.registry.internal"
scheme = "https"
methods = ["GET"]
ports = [443]
The build toolchain (e.g., agent-cli bundle) parses this manifest, validates syntax, and embeds the capability table into a custom section of the Wasm binary. Components with missing declarations or empty grant lists are rejected at load time, preventing implicit ambient access.
2. Operator Runtime Constraints
Operators define runtime constraints via CLI flags or configuration profiles. These constraints represent the operator's security posture and can be more restrictive than the component's declaration but never more permissive.
Runtime Configuration:
agent run data-processor.wasm \
--fs-policy allowlist \
--fs-allow "/var/db/processing/results.db" \
--http-policy allowlist \
--http-allow "host=config.registry.internal;scheme=https" \
--http-deny "cidr=169.254.169.254/32"
Alternatively, operators can define reusable profiles in ~/.config/agent/policy.toml:
[profile.production.policy]
fs = { mode = "allowlist", allow = [
{ path = "/var/db/processing/results.db", mode = "read-write" },
]}
http = { mode = "allowlist", allow = [
{ host = "config.registry.internal", scheme = "https" },
], deny_cidr = ["169.254.169.254/32"] }
3. Effective Policy Computation
At component load time, the host runtime computes the effective policy by intersecting the declared capabilities with the operator's constraints. This computation occurs before any WASI imports are wired up.
Runtime Implementation (policy_engine.rs):
pub struct PolicyEngine {
declared: CapabilityManifest,
operator: RuntimeConstraints,
}
impl PolicyEngine {
pub fn compute_effective(&self) -> EffectivePolicy {
let fs_effective = self.intersect_fs();
let http_effective = self.intersect_http();
EffectivePolicy {
filesystem: fs_effective,
http: http_effective,
}
}
fn intersect_fs(&self) -> FsAccessScope {
// Intersection logic: only grants present in both
// declared and operator sets are retained.
// Operator grants outside the declared ceilin
g are dropped. self.operator.fs.allow .iter() .filter(|op_grant| self.declared.fs.is_covered_by(op_grant)) .cloned() .collect() } }
If the intersection results in an empty set for a required capability, the component fails to load. This ensures that the component cannot run with insufficient permissions, and the operator cannot grant unauthorized access.
### 4. DNS-Layer Enforcement
HTTP policy enforcement extends to the DNS resolution phase to prevent metadata service access and IP-based bypasses. A custom DNS resolver intercepts resolution requests and validates resolved IPs against operator-defined CIDR deny rules before the TCP connection is initiated.
**Secure Resolver (`dns_resolver.rs`):**
```rust
pub struct SecureResolver {
deny_cidrs: Vec<IpNet>,
}
impl Resolve for SecureResolver {
fn resolve(&self, name: &str) -> Result<Vec<IpAddr>, DnsError> {
let ips = upstream_resolve(name)?;
for ip in &ips {
if self.is_denied(ip) {
// Return DnsError to mask existence of the IP
return Err(DnsError::PolicyViolation);
}
}
Ok(ips)
}
fn is_denied(&self, ip: &IpAddr) -> bool {
self.deny_cidrs.iter().any(|cidr| cidr.contains(ip))
}
}
Returning a DnsError instead of a connection refusal prevents attackers from distinguishing between non-existent hosts and blocked hosts, mitigating information leakage. This is critical for blocking cloud metadata endpoints like 169.254.169.254.
5. Per-Hop Redirect Validation
Naive allowlists are vulnerable to redirect smuggling, where an allowed host redirects to a denied host. The HTTP client implementation includes a custom redirect policy that re-evaluates every hop in the redirect chain against the effective policy.
Redirect Policy (http_client.rs):
fn build_redirect_policy(policy: &EffectivePolicy) -> reqwest::redirect::Policy {
reqwest::redirect::Policy::custom(move |attempt| {
let url = attempt.url();
if policy.http.is_allowed(url) {
attempt.follow()
} else {
attempt.stop()
}
})
}
This ensures that a 302 redirect to a denied domain fails immediately, closing the redirect-smuggling bypass vector.
6. Ancestor-Aware Filesystem Matching
WASI path resolution requires statting intermediate directories when opening nested files. A naive policy that allows /var/db/processing/results.db would fail because the runtime cannot stat /var/db or /var/db/processing. The filesystem matcher implements implicit ancestor traversal for allowed leaf paths.
Path Matcher (fs_matcher.rs):
pub struct PathMatcher {
allowed_prefixes: Vec<PathBuf>,
}
impl PathMatcher {
pub fn check(&self, target: &Path) -> AccessDecision {
if self.allowed_prefixes.iter().any(|prefix| {
target.starts_with(prefix) || self.is_ancestor(target, prefix)
}) {
AccessDecision::Allow
} else {
AccessDecision::Deny
}
}
fn is_ancestor(&self, target: &Path, prefix: &Path) -> bool {
// Allows traversal through ancestors of an allowed leaf
prefix.ancestors().any(|ancestor| target.starts_with(ancestor))
}
}
This logic permits directory traversal to reach allowed files while maintaining strict denial for sibling files or unauthorized directories.
Pitfall Guide
1. The Isolation-Authorization Fallacy
Explanation: Assuming that wasmtime isolation eliminates the need for capability policy. Isolation prevents raw syscalls, but WASI imports provide controlled access to resources. Without policy, imports can be wired up with broad permissions.
Fix: Always deploy the policy layer. Treat WASI imports as capability gates that must be filtered by the intersection of declaration and operator intent.
2. Redirect Smuggling Bypass
Explanation: Implementing HTTP allowlists that only check the initial request URL. Attackers can use an allowed host to redirect requests to denied hosts, bypassing the policy. Fix: Implement per-hop redirect validation. Every URL in the redirect chain must be evaluated against the effective policy before following the redirect.
3. DNS Leakage via Metadata IPs
Explanation: Relying solely on hostname allowlists. Attackers can resolve allowed hostnames to IPs that include metadata service addresses, or use DNS rebinding to access internal networks.
Fix: Implement DNS-layer enforcement. Validate all resolved IPs against CIDR deny rules before connection. Return DnsError for blocked IPs to prevent existence probing.
4. WASI Stat Traversal Failures
Explanation: Defining filesystem allowlists with leaf paths only. WASI requires stat access to parent directories, causing legitimate file opens to fail with permission errors. Fix: Use ancestor-aware matching. When a leaf path is allowed, implicitly permit traversal through its ancestor directories without granting access to sibling files.
5. Empty Allow List Ambiguity
Explanation: Declaring a capability in the manifest but providing no allow rules. This can lead to undefined behavior or accidental open access if the runtime defaults to permissive mode. Fix: Enforce hard denial for empty allow lists. The build toolchain should reject manifests with empty grants, and the runtime should fail to load components with unresolved capability requirements.
6. Operator Over-Granting
Explanation: Operators attempting to grant permissions beyond the component's declared ceiling via runtime flags. This creates a false sense of security if the runtime silently accepts the excess grants. Fix: Implement strict intersection logic. The runtime must drop any operator grants that exceed the declared ceiling and log warnings for such mismatches.
7. Build-Time vs. Run-Time Drift
Explanation: Modifications to the component binary after build time that alter the embedded capability manifest, or running components without validating the manifest against the source. Fix: Verify manifest integrity at load time. Use cryptographic hashes or signatures to ensure the embedded manifest matches the expected build artifact. Reject components with tampered manifests.
Production Bundle
Action Checklist
- Validate Manifests: Ensure all components include a valid capability manifest with explicit grants. Reject builds with missing or empty declarations.
- Define Operator Profiles: Create runtime policy profiles for different environments (e.g., dev, staging, prod) using allowlists and CIDR deny rules.
- Test Redirect Chains: Verify that HTTP policies block redirects to denied hosts by testing components with redirect endpoints.
- Audit DNS Resolution: Confirm that DNS-layer enforcement blocks metadata IPs and returns
DnsErrorfor denied ranges. - Verify Ancestor Traversal: Test filesystem access with nested paths to ensure implicit ancestor traversal works correctly without over-granting.
- Monitor Intersection Logs: Enable logging for policy intersection events to detect mismatches between declarations and operator grants.
- Sign Components: Use signing mechanisms to ensure component integrity and prevent manifest tampering in the supply chain.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Internal Trusted Tools | Allowlist with broad declaration ceiling | Reduces operational friction while maintaining auditability. | Low |
| Public Third-Party Tools | Strict allowlist with narrow declaration ceiling | Minimizes blast radius for untrusted code. | Medium |
| High-Security Workloads | Intersection with CIDR deny and DNS enforcement | Prevents metadata access and network pivoting. | High |
| Development/Testing | Relaxed intersection with verbose logging | Facilitates debugging without compromising production standards. | Low |
Configuration Template
agent-policy.toml (Production Profile):
[profile.production.policy]
# Filesystem: Strict allowlist
fs = { mode = "allowlist", allow = [
{ path = "/var/db/processing/results.db", mode = "read-write" },
{ path = "/tmp/work/**", mode = "read-write" },
]}
# HTTP: Allowlist with CIDR deny
http = { mode = "allowlist", allow = [
{ host = "config.registry.internal", scheme = "https", methods = ["GET"], ports = [443] },
], deny_cidr = [
"169.254.169.254/32",
"10.0.0.0/8",
]}
# Logging: Enable policy intersection logs
logging = { level = "info", policy_events = true }
Quick Start Guide
- Install CLI: Install the agent runtime and build tools (
agent-cli). - Create Manifest: Define capabilities in
agent-tool.tomlwith explicit grants for filesystem and HTTP access. - Build Component: Run
agent-cli bundleto validate the manifest and embed capabilities into the Wasm binary. - Run with Policy: Execute the component using
agent runwith operator constraints or a policy profile. - Verify Enforcement: Check logs for policy intersection events and test access to denied resources to confirm enforcement.
