Add WASM sandbox secure API extension

Extends the WASM sandbox with HTTP API capabilities, secrets management,
tool aliasing, and leak detection. Key security principle: WASM never
sees credentials, injection happens at host boundary.

New modules:
- secrets: AES-256-GCM encrypted storage with HKDF key derivation
- leak_detector: Aho-Corasick + regex pattern matching for secret exfiltration
- capabilities: Extended capability system (HTTP, ToolInvoke, Secrets)
- allowlist: HTTP endpoint validation with glob patterns
- credential_injector: Host-boundary credential injection
- rate_limiter: Sliding window per-tool rate limiting
- storage: WASM binary storage with BLAKE3 integrity verification

Leak detection happens at two points:
1. Before HTTP request (prevents exfiltration via URL/headers/body)
2. After response (prevents exposure in outputs returned to WASM)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-02 23:22:52 -08:00
co-authored by Claude Opus 4.5
parent 45bbfa026d
commit 32bfd24154
21 changed files with 5115 additions and 66 deletions
+48
View File
@@ -16,6 +16,7 @@ pub struct Config {
pub agent: AgentConfig,
pub safety: SafetyConfig,
pub wasm: WasmConfig,
pub secrets: SecretsConfig,
}
impl Config {
@@ -31,6 +32,7 @@ impl Config {
agent: AgentConfig::from_env()?,
safety: SafetyConfig::from_env()?,
wasm: WasmConfig::from_env()?,
secrets: SecretsConfig::from_env()?,
})
}
}
@@ -250,6 +252,52 @@ pub struct WasmConfig {
pub cache_dir: Option<PathBuf>,
}
/// Secrets management configuration.
#[derive(Clone, Default)]
pub struct SecretsConfig {
/// Master key for encrypting secrets (loaded from SECRETS_MASTER_KEY env var).
/// Must be at least 32 bytes for AES-256-GCM.
pub master_key: Option<SecretString>,
/// Whether secrets management is enabled.
pub enabled: bool,
}
impl std::fmt::Debug for SecretsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretsConfig")
.field("master_key", &self.master_key.is_some())
.field("enabled", &self.enabled)
.finish()
}
}
impl SecretsConfig {
fn from_env() -> Result<Self, ConfigError> {
let master_key = optional_env("SECRETS_MASTER_KEY")?.map(SecretString::from);
let enabled = master_key.is_some();
// Validate master key length if provided
if let Some(ref key) = master_key {
if key.expose_secret().len() < 32 {
return Err(ConfigError::InvalidValue {
key: "SECRETS_MASTER_KEY".to_string(),
message: "must be at least 32 bytes for AES-256-GCM".to_string(),
});
}
}
Ok(Self {
master_key,
enabled,
})
}
/// Get the master key if configured.
pub fn master_key(&self) -> Option<&SecretString> {
self.master_key.as_ref()
}
}
impl Default for WasmConfig {
fn default() -> Self {
Self {