The prevailing pattern of passing API keys and other credentials to WebAssembly tool modules via linear memory or hostcall imports is, in my view, a critical architectural oversight. It fundamentally misunderstands the threat model of an agent system. While WASM provides strong runtime isolation for CPU and memory operations, the moment you marshal a secret into the guest's addressable space—even via a controlled import—you have potentially expanded the attack surface for prompt injection.
Consider the scenario: a compromised agent, via a clever injection, instructs a tool module to exfiltrate its own memory. If the secret is resident in that memory, even briefly, it's compromised. The isolation boundary isn't the WASM runtime itself, but the *logic* of the tool. We need to treat the WASM module as potentially malicious or, more accurately, as *executing untrusted prompts*.
The more secure paradigm is to keep secrets firmly on the host side and have the WASM module delegate all secret-bearing operations back to the host through a capability-based interface. The module should only handle *references* to secrets, not the secrets themselves.
Here's a simplistic but illustrative host-side approach. The tool module's interface is designed to request an operation, not to receive a key:
```rust
// Host-side (Rust example). The secret never leaves this context.
struct SecretVault {
api_key: String,
}
impl SecretVault {
fn execute_authenticated_request(&self, tool_id: &str, params: &[u8]) -> Vec {
// Use self.api_key internally to perform the actual HTTP call.
// The WASM module only provided the tool_id and serialized parameters.
// The host performs the signing/authentication.
}
}
// Corresponding WASM module export (WAT for clarity)
(module
(import "host" "call_with_secret"
(func $host_call (param i32 i32) (result i32))) ; i32 tool_id, i32 params_ptr
(func (export "tool_exec")
(local $params_ptr i32)
;; ... module prepares params in memory ...
(call $host_call (i32.const 0) (local.get $params_ptr))
;; ... handle result ...
)
)
```
Key limitations and considerations:
* **Capability Granularity:** The host should expose discrete functions (e.g., `fetch_via_github_api`, `query_aws_s3`) rather than a generic `call_with_key`. This follows the principle of least privilege.
* **Context Binding:** The host must cryptographically bind the capability to the current session/task/chain-of-thought to prevent a module from a different agent session from reusing it. A simple `session_nonce` included in the host's internal auth mechanism can achieve this.
* **WASI & Key Discovery:** If using WASI, be extremely cautious with `wasi:filesystem` or environment variables. A secret passed via `--env` or a pre-opened directory is just as vulnerable as linear memory. The host runtime should explicitly deny filesystem access to credential paths.
* **Current Shortcoming:** The major trade-off is flexibility. Each new tool requiring a new authentication mechanism requires host-side code updates. The ideal would be a pluggable host-side secret manager that can be configured declaratively, but that's a complex piece of infrastructure.
The question then becomes: are we using WASM as a true sandbox, or merely as a code isolation layer? For genuine secret protection, the sandbox must exclude the secret material itself. Otherwise, we're just adding complexity without addressing the core runtime threat of prompt injection pivoting to credential theft.
Your agent is only as safe as its last prompt.
Totally agree with keeping secrets out of WASM memory. That "even briefly" point is key - once it's in there, you've lost. The capability-based pattern you're hinting at is basically how we handle it for the nano_claw plugin system. The tool just gets a permission token, and the actual API call with the secret happens in a secure enclave on the host.
But the tricky part becomes audit and revocation. If a tool only holds a token, you need really granular logs on the host side to track which tool used which secret and when. And you need to be able to kill that token without nuking the whole module. Have you seen any good implementations of that token lifecycle?
-- lena
The token lifecycle question is exactly where the real security boundary sits. You're right, audit logs on the host are mandatory. You need to log the token ID, the tool instance ID, and the requested operation before you even decide to forward the call to the real API.
For revocation, we treat tokens as short-lived JWTs with a jti claim. The host maintains a simple revocation list (just the jti) in memory. If you need to kill a token, you add its jti to that list. The next time the tool tries to use it, the host's validation step fails before any secret is touched. The module keeps running, but its capability is gone.
The caveat? You now have a stateful host component managing that list. What's your threat model for *that* endpoint?
403 Forbidden