Skip to content

Latest commit

 

History

History
168 lines (117 loc) · 5.78 KB

File metadata and controls

168 lines (117 loc) · 5.78 KB

chisel-core

Portable, synchronous Rust library providing path-confined filesystem operations and whitelisted shell execution. Zero async, zero HTTP, zero MCP protocol — drop it into any server and own the transport entirely.

This is the enforcement layer that backs the chisel MCP server. Every security property (kernel-enforced root confinement, atomic writes, shell whitelist) is implemented here and applies identically whether you use the standalone server or embed the library directly.


When to use this instead of the server

Scenario Use
You are writing a Rust MCP server and want identical safety semantics without running a sidecar chisel-core directly
You are writing an MCP server in Node.js, Python, Deno, or any WASI runtime chisel-wasm (this library compiled to wasm32-wasip1)
You just want a ready-to-run MCP server chisel standalone binary

API

All functions take a root: &Path as their first argument. Every path operation is confined to that root via cap_std — kernel-enforced at the openat level, not a userspace prefix check.

Filesystem operations

use std::path::Path;
use chisel_core::ops::filesystem::{write_file, patch_apply, append, create_directory, move_file};

let root = Path::new("/data");

// Create or overwrite a file (parent dirs created automatically)
write_file(root, "/data/hello.txt", "hello world\n", /*read_only=*/false)?;

// Apply a unified diff atomically — hunk mismatch returns PatchFailed, file untouched
patch_apply(root, "/data/hello.txt",
    "--- a\n+++ b\n@@ -1 +1 @@\n-hello world\n+goodbye world\n",
    /*read_only=*/false)?;

// Append to an existing file (file must already exist)
append(root, "/data/hello.txt", "\nappended line\n", /*read_only=*/false)?;

// mkdir -p semantics
create_directory(root, "/data/sub/dir", /*read_only=*/false)?;

// Move or rename within root
move_file(root, "/data/old.txt", "/data/new.txt", /*read_only=*/false)?;

Shell execution

use chisel_core::ops::shell::shell_exec;

// Whitelisted commands only: grep rg sed awk find cat head tail wc sort uniq cut tr diff file stat ls du
let out = shell_exec(root, "grep", &["-n", "goodbye", "/data/hello.txt"])?;
println!("exit={} stdout={}", out.exit_code, out.stdout);

Commands are spawned via std::process::Command directly — no shell interpreter, so metacharacters (|, &&, $(), etc.) in arguments are passed as literals. Path-like arguments are validated against root before the process starts.

shell_exec is not available in the WASM build (chisel-wasm). Process spawning is not a WASI capability.


Error types

Variant Condition
OutsideRoot Resolved path escapes the configured root
NotFound File or directory does not exist
PatchFailed Hunk context does not match current file content
ReadOnly Write attempted with read_only = true
CommandNotAllowed Command not in the compile-time whitelist
PermissionDenied OS-level permission denied

Embedding in Rust

Add to your Cargo.toml:

[dependencies]
chisel-core = { path = "../chisel-core" }   # or publish to crates.io and use a version

Inside a synchronous handler, call directly. Inside an async handler, wrap with spawn_blocking:

let result = tokio::task::spawn_blocking(move || {
    chisel_core::ops::filesystem::write_file(&root, &path, &content, read_only)
}).await??;

Embedding via WASM (Node.js / Python / Deno)

Build chisel-wasm targeting wasm32-wasip1:

rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release -p chisel-wasm
# artifact: target/wasm32-wasip1/release/chisel_wasm.wasm

Node.js (≥ 22)

import { readFile } from "node:fs/promises";
import { WASI } from "node:wasi";
import { argv, env } from "node:process";

const wasi = new WASI({
  version: "preview1",
  args: argv,
  env,
  preopens: { "/data": "/path/to/your/data" },
});

const wasm = await WebAssembly.compile(
  await readFile("target/wasm32-wasip1/release/chisel_wasm.wasm")
);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance);

// Call exported functions via instance.exports
// write_file(root_ptr, path_ptr, content_ptr, read_only) -> result_ptr

Python

pip install wasmtime
from wasmtime import Store, Module, Linker, WasiConfig

store = Store()
config = WasiConfig()
config.preopen_dir("/path/to/your/data", "/data")

linker = Linker(store.engine)
linker.define_wasi()
store.set_wasi(config)

module = Module.from_file(store.engine, "target/wasm32-wasip1/release/chisel_wasm.wasm")
instance = linker.instantiate(store, module)

exports = instance.exports(store)
# write_file, patch_apply, append, create_directory, move_file are available

Security properties

All properties are enforced at this layer regardless of how the library is embedded.

# Property Mechanism
1 Kernel-enforced root confinement — directory traversal, symlink escape, TOCTOU all blocked cap_std::fs::Dir; every component traversed via openat(fd, component, O_NOFOLLOW)
2 Atomic writes — failed patch never corrupts the target file Dir::create(".name.PID.tmp") + Dir::rename(tmp → target); on failure tmp is discarded
3 Read-only mode — blanket write protection check_writable(read_only) runs before any I/O in every write op
4 Shell whitelist + direct execve — no injection, no arbitrary commands Fixed compile-time whitelist; std::process::Command spawns directly (no sh -c)

See the full security model in the root README for the complete breakdown including attack scenarios and test coverage.