The principle of least privilege is older than most of the people reading this. Saltzer and Schroeder formalised it in 1975: every program and every user should operate using the least set of privileges necessary to complete the job. Fifty years later, AI agent tools still run with the full authority of their host process.
MPP's permission system exists to close that gap. It is a capability-based model — not a role-based model, not an ACL, not a policy-as-code engine bolted on after the fact — built into the package format and enforced by the runtime at the syscall boundary.
What "Capability-Based" Means
In a traditional access control system, a subject (a user or process) has an identity, and access decisions are made by checking that identity against a central policy. The process calls open("/etc/shadow"), the kernel checks the process's UID against the file's permissions, and allows or denies the request.
In a capability-based system, access is determined by possession of a capability token. If you have a token granting read access to /data/inputs, you can read from /data/inputs. If you don't have the token, the resource doesn't exist for you — not "access denied," but genuinely invisible.
The distinction matters because it changes the failure mode. In an identity-based system, misconfiguration grants too much access (the process has root, and root can do anything). In a capability-based system, misconfiguration grants too little access (the tool doesn't have the token, so it can't reach the resource). The system fails safe.
MPP implements capabilities as a four-stage pipeline: declaration → evaluation → approval → enforcement.
Stage 1: Declaration
Every MPP tool declares the resources it needs in its manifest. This declaration is not optional, not advisory, and not a hint. It is the complete list of external resources the tool will access at runtime.
{
"capabilities": {
"network": ["api.github.com", "api.openai.com"],
"filesystem": {
"read": ["/data/inputs", "/config"],
"write": ["/data/outputs"]
},
"env_vars": ["GITHUB_TOKEN", "OPENAI_API_KEY"]
},
"kv_store": {
"enabled": true,
"max_size_mb": 10
}
}
There are five capability types:
| Capability | Declaration | What It Grants |
|-----------|-------------|---------------|
| Network(domain) | capabilities.network | HTTP/HTTPS access to a specific FQDN |
| FilesystemRead(path) | capabilities.filesystem.read | Read-only access to a guest path |
| FilesystemWrite(path) | capabilities.filesystem.write | Read-write access to a guest path |
| EnvVar(name) | capabilities.env_vars | Access to a specific environment variable |
| KvStore | kv_store.enabled | Per-package persistent key-value storage |
The declarations are granular. Network access is per-domain, not "network: yes." Filesystem access is per-path, with separate read and write declarations. Environment variables are named individually. There is no "grant all" wildcard.
This granularity is the point. A tool that analyses GitHub pull requests needs api.github.com and GITHUB_TOKEN. It does not need api.stripe.com. It does not need write access to the filesystem. It does not need AWS_SECRET_ACCESS_KEY. If it declares those capabilities, that declaration is visible to every human and automated system that evaluates the tool — and it raises an immediate question about why a PR analysis tool needs access to your payment processor.
Stage 2: Evaluation
When a tool is invoked, the host's PermissionPolicy engine evaluates the tool's declared capabilities against organisational policy. The evaluation produces one of three outcomes:
pub enum PolicyDecision {
Granted(CapabilityToken), // Proceed — auto-approved
RequiresConfirmation { // Needs human approval
capabilities: Vec<Capability>,
security_level: SecurityLevel,
},
Denied { reason: String }, // Blocked — policy violation
}
The evaluation logic is straightforward:
Auto-approved if the package is in the trusted packages list:
let policy = PermissionPolicy::new()
.trust_package("org.mycompany.internal-tool")
.trust_package("com.verified-vendor.data-tool");
Trusted packages bypass HITL confirmation. This is the mechanism for enterprise IT teams to pre-approve internal tools so that developers and agents aren't prompted for every invocation.
Auto-approved if auto-approve-low is enabled and the tool has a low security level with no capabilities:
let policy = PermissionPolicy::new()
.auto_approve_low(true);
Read-only, self-contained tools that don't touch the network, filesystem, or environment are low risk by definition. Auto-approving them eliminates friction without introducing meaningful exposure.
Requires confirmation for everything else. The host displays the tool's capability declarations to the user and asks for explicit approval before proceeding.
Stage 3: Approval (Human-in-the-Loop)
When a tool requires confirmation, MPP doesn't just show a generic "Do you want to run this tool?" prompt. It computes a sensitivity score that determines the appropriate level of confirmation.
The score is calculated from the tool's declared security level and its requested capabilities using a deterministic formula:
| Component | Points |
|-----------|--------|
| Security level: low | 5 |
| Security level: medium | 25 |
| Security level: high | 55 |
| Security level: critical | 85 |
| Any filesystem write | +10 |
| Each write path (up to 5) | +2 |
| Each network domain (up to 5) | +5 + 2 each |
| Each environment variable (up to 5) | +5 + 2 each |
| Requires agent identity | +5 |
| Cap | 100 |
The score maps to four confirmation tiers:
| Score | Level | User Experience | |-------|-------|----------------| | 0–19 | None | Silent execution — no prompt | | 20–49 | Notify | Informational notification; tool runs immediately | | 50–79 | Confirm | User must click "Allow" to proceed | | 80–100 | MultiFactor | Multiple confirmation steps |
Examples
A read-only text formatter (security: low, no capabilities) scores 5 → runs silently.
A GitHub PR analyser (security: medium, two network domains, one env var) scores 25 + 5 + 2 + 5 + 2 + 5 + 2 = 46 → informational notification.
A database migration tool (security: high, one domain, two write paths, two env vars) scores 55 + 10 + 4 + 5 + 2 + 5 + 2 + 5 + 2 + 2 = 92 → multifactor confirmation.
The scoring is transparent and auditable. A security team can review the formula, model the scores for their tool portfolio, and adjust policy (trusted package lists, auto-approve thresholds) to match their risk appetite.
Attestation
After human approval, the host issues an AttestationToken — an ephemeral, single-use, Ed25519-signed proof that a specific invocation was approved:
pub struct AttestationToken {
pub attestation_id: String,
pub package_id: String,
pub tool: String,
pub user_id: String,
pub approved_at: String, // ISO 8601
pub expires_at: String, // ISO 8601 — 30-second TTL
pub capabilities_granted: Vec<String>,
pub nonce: Option<String>,
pub signature: Option<String>, // Ed25519
}
The token has a 30-second TTL and can only be used once. If a tool tries to reuse a token (replay attack), the nonce tracker rejects it. The token is scoped to a specific package, tool, and set of capabilities — it cannot be repurposed.
Stage 4: Enforcement
This is where capability-based security diverges most sharply from traditional models. The capability token is not checked against a policy database at runtime. Instead, it configures the sandbox itself.
When the WASM sandbox is instantiated, the host reads the CapabilityToken and provisions the sandbox with exactly the resources listed:
- Network domains in the token → configured as the allow-list in the network filter host functions. Requests to any other domain return error code
-1. - Filesystem read paths → mounted as WASI pre-opened directories, read-only. Paths not in the token are not mounted and do not exist in the tool's filesystem namespace.
- Filesystem write paths → mounted as WASI pre-opened directories, read-write.
- Environment variables → injected into the WASI environment. Variables not in the token are absent from the environment.
- KV store → if granted, the
MPP_KV_PATHvariable is set to the package's isolated storage path.
// The sandbox only sees what the token allows
let config = SandboxConfig {
env_vars: token.granted_env_vars(),
dir_mappings: token.granted_dir_mappings(&host_paths),
allowed_network_domains: token.granted_domains(),
capability_token: Some(token),
// ...
};
The enforcement is not a check that can be bypassed. The resources physically do not exist in the sandbox unless the token grants them. A tool that attempts to open("/etc/passwd") doesn't get a "permission denied" error — the path doesn't resolve to anything. A tool that calls mpp_net::fetch("https://api.stripe.com/...") gets error code -1 because the domain isn't in the filter.
This is the key difference from ACL-based systems. In an ACL system, the resource exists and a guard decides whether to allow access. In the capability model, ungrantable resources are absent. The attack surface isn't restricted — it's removed.
What This Looks Like in Practice
Low-Risk Tool: Text Formatter
{
"capabilities": {
"network": [],
"filesystem": { "read": [], "write": [] },
"env_vars": []
},
"tool_definitions": [{
"name": "format_markdown",
"security_level": "low"
}]
}
Sensitivity score: 5. No capabilities. Runs silently. The sandbox is instantiated with no network, no filesystem, no environment. The tool receives input via stdin, produces output via stdout, and touches nothing else.
Medium-Risk Tool: API Integration
{
"capabilities": {
"network": ["api.salesforce.com"],
"filesystem": { "read": [], "write": [] },
"env_vars": ["SF_API_TOKEN"]
},
"tool_definitions": [{
"name": "query_accounts",
"security_level": "medium"
}]
}
Sensitivity score: 39. One network domain and one env var. The user sees an informational notification: "query_accounts wants to access api.salesforce.com and read SF_API_TOKEN." The sandbox mounts no filesystem paths, injects the single environment variable, and configures the network filter to allow only api.salesforce.com.
High-Risk Tool: Infrastructure Management
{
"capabilities": {
"network": ["api.aws.amazon.com", "monitoring.internal.corp"],
"filesystem": {
"read": ["/config"],
"write": ["/data/terraform-state"]
},
"env_vars": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"]
},
"tool_definitions": [{
"name": "apply_infrastructure",
"security_level": "critical"
}]
}
Sensitivity score: 85+. Multiple network domains, filesystem writes, and credential access on a critical-security tool. The user sees a multifactor confirmation dialog listing every capability. The sandbox mounts /config read-only, /data/terraform-state read-write, injects the three AWS variables, and filters network access to the two declared domains.
The Audit Trail
Every permission decision is logged. Not just the outcome — the entire chain:
- Which capabilities were declared in the manifest
- How the PermissionPolicy evaluated them (granted, requires confirmation, or denied)
- What the sensitivity score was and what confirmation level it triggered
- Whether the user approved or denied (and the timestamp of their decision)
- What capabilities were actually granted in the CapabilityToken
- Whether the attestation token was valid when the sandbox consumed it
This log is the foundation for governance. A compliance team can answer the question "who approved this tool to access our AWS credentials?" by looking at a single log entry. An incident responder can reconstruct the exact set of capabilities a compromised tool had access to.
Enterprise Policy Patterns
For organisations deploying MPP at scale, the permission system supports several policy patterns:
Internal tools auto-approved. Add your organisation's packages to the trusted list. Developers using internal tools won't see prompts.
let policy = PermissionPolicy::new()
.trust_package("org.mycompany.data-pipeline")
.trust_package("org.mycompany.code-reviewer")
.trust_package("org.mycompany.deploy-tool");
Low-risk external tools auto-approved. Enable auto-approve-low for capability-free, low-security tools from external publishers. Useful for utilities like formatters, validators, and converters.
Everything else requires confirmation. By default, any tool not on the trusted list and not auto-approved triggers HITL. The sensitivity scoring ensures the confirmation experience is proportional to the risk.
Block specific capabilities. Policy can be extended to deny specific capability patterns — for example, blocking any tool that requests environment variable access to AWS_* keys unless it's from a trusted publisher.
The permission system is designed to get out of the way for low-risk operations and demand attention for high-risk ones. That balance — between agent autonomy and security control — is what makes AI tool execution viable for enterprise production.
For the full permission API, see the Permission System documentation. For how capabilities are enforced in the sandbox, read WASM Sandboxing Explained.