Living Document Notice
Published 2026-09-18. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.
Sandboxing Client Extensions with Outrigger
Summary
Community extensions and third-party plugins drive user customization in desktop personal knowledge management tools. However, conventional plugin architectures execute third-party JavaScript directly within the main application execution context. This gives unvetted community scripts unrestricted access to the DOM, local storage databases, filesystem handles, and outbound network sockets, introducing catastrophic attack vectors for data theft and silent file modification.
FreeNext isolates third-party extensions using the Outrigger Protocol execution runtime. By running plugins inside capability-bounded WebAssembly instances, extensions can only interact with local vault data through explicitly declared, user-approved system interfaces.
Capability-Based Sandboxing Architecture
Under Outrigger, a plugin binary possesses zero intrinsic permissions. It cannot execute disk I/O, allocate unbounded host memory, or establish network connections unless the host environment explicitly provides imported capability handles.
+-------------------------------------------------------------+
| FreeNext Host Runtime |
| - Master SQLite Database Handle |
| - Full Vault Filesystem Access |
+-------------------------------------------------------------+
|
| Mediated Host Boundaries
v
+-------------------------------------------------------------+
| Outrigger Capability Broker |
| - Validates Extension Manifest Permissions |
| - Injects Scoped Read-Only SQLite Virtual Tables |
| - Blocks Outbound Sockets by Default |
+-------------------------------------------------------------+
|
| WASI / Wasmtime Memory Sandbox
v
+-------------------------------------------------------------+
| Sandboxed WASM Extension |
| (e.g., Graph Renderer, Word Counter) |
+-------------------------------------------------------------+
If an extension attempts to read paths outside its assigned manifest scope, the WebAssembly runtime traps the execution thread and logs a security exception without endangering host data.
Capability Permission Matrix
Outrigger enforces granular capability boundaries rather than all-or-nothing installation prompts.
| Capability Identifier | Granted Scope | Default State | Enforcement Mechanism |
|---|---|---|---|
vault:read:metadata | Note titles, frontmatter tags, word counts | Permitted | Read-only SQLite view restricting body text |
vault:read:content | Full CommonMark document contents | Prompt Required | Path-scoped file descriptor handles |
vault:write:patch | Proposing unified diffs to active note | Prompt Required | Transaction staging buffer prior to user review |
network:outbound | HTTP fetch to external domains | Blocked | Complete absence of network syscall host imports |
host:clipboard | Reading system clipboard buffer | Blocked | Explicit user confirmation per write operation |
compute:fuel | CPU execution limits per cycle | 50M Fuel Units | WebAssembly instruction counting trap |
By default, an analytical extension like an alternative graph renderer receives only vault:read:metadata. It cannot exfiltrate private note prose or contact remote telemetry collectors.
Outrigger Host Import Interface
The host runtime exposes a constrained Foreign Function Interface (FFI) to the guest WebAssembly module using standard WebAssembly System Interface (WASI) conventions:
// outrigger_host.rs: Host interface providing capability-gated database access
use wasmtime::*;
pub struct OutriggerPluginContext {
pub extension_id: String,
pub allowed_paths: Vec<String>,
pub execution_fuel_remaining: u64,
}
pub fn bind_outrigger_host_imports(
linker: &mut Linker<OutriggerPluginContext>,
) -> Result<(), wasmtime::Error> {
linker.func_wrap(
"outrigger_v1",
"query_note_tags",
|mut caller: Caller<'_, OutriggerPluginContext>, path_ptr: i32, path_len: i32, out_ptr: i32| -> i32 {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
let ctx = caller.data();
let mut path_buf = vec![0u8; path_len as usize];
memory.read(&caller, path_ptr as usize, &mut path_buf).unwrap();
let requested_path = String::from_utf8_lossy(&path_buf);
if !ctx.allowed_paths.iter().any(|p| requested_path.starts_with(p)) {
return -1; // Permission Denied code
}
let tags_json = r#"["dispatch", "architecture"]"#;
let bytes = tags_json.as_bytes();
memory.write(&mut caller, out_ptr as usize, bytes).unwrap();
bytes.len() as i32
},
)?;
Ok(())
}Isolating community plugins within capability-governed WebAssembly sandboxes enables modular extensibility while protecting user notes against exfiltration and unauthorized modification.
- Directus Target: freenext
- Garden Source Reference: OUT-1001 - Zero Dependency Sandboxing, NXT-1001 - The Application Is Just a Lens, MOC - Data Liberation Workbenches, MOC - The Plain-Text Longevity Standard, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools, MOC - Outrigger Protocol