Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust library

The execkit crate is the core. The MCP server is a thin wrapper over it; you can embed the same sessions directly in your own program.

[dependencies]
execkit = "0.9"                                          # local + SSH + Docker
# execkit = { version = "0.9", default-features = false } # local + Docker only (no SSH; drops russh/tokio)
use execkit::{Session, Policy};

fn main() -> Result<(), execkit::Error> {
    let mut s = Session::local()?
        .with_policy(Policy { allow: vec![], deny: vec!["rm".into()] });

    let r = s.exec("echo hi; echo err 1>&2; cd /tmp")?;
    // r.stdout == "hi", r.stderr == "err", r.exit_code == 0, r.cwd == "/tmp"
    println!("{} (exit {})", r.stdout, r.exit_code);
    Ok(())
}

State persists across exec calls on the same Session, exactly as it does over MCP. Results are the same structured ExecResult (split stdout/stderr, exit code, duration, cwd, truncated, timed_out), ANSI-stripped and secret-redacted.

Timeouts

Each exec has a timeout: 30 seconds by default. Change it with with_timeout when building the session, set_timeout on a live one, or pass one for a single call with exec_with_timeout:

#![allow(unused)]
fn main() {
use std::time::Duration;

let mut s = Session::local()?.with_timeout(Duration::from_secs(60));
let r = s.exec_with_timeout("sleep 30", None, Duration::from_secs(1))?;
assert!(r.timed_out);            // interrupted with Ctrl-C
assert_eq!(r.exit_code, 124);
assert_eq!(s.exec("echo still here")?.stdout, "still here");
}

A timed-out command is interrupted with Ctrl-C and returned as Ok with timed_out: true; the session keeps its cwd and env. If the command ignores Ctrl-C, exec returns Error::StillRunning and the session is poisoned: later calls return Error::SessionPoisoned and is_poisoned() is true. A command that exits the shell (exit, or a failure under set -e) returns Error::ShellExited and poisons it the same way. Open a new session in either case.

SSH and Docker

SSH and Docker sessions are constructed with their configs:

#![allow(unused)]
fn main() {
use execkit::{Session, SshConfig, SshAuth, HostKeyVerification};

let cfg = SshConfig::new("web-01", "deploy",
    SshAuth::Password("...".into()),
    HostKeyVerification::KnownHosts("/home/me/.execkit/known_hosts".into()));
let mut s = Session::ssh(cfg)?;
}

The known_hosts file uses execkit’s own host SHA256:<fingerprint> format (keyed [host]:port on ports other than 22), not OpenSSH’s; use a file of its own. SshConfig::connect_timeout (default 15 seconds) bounds the TCP connect, key exchange and authentication together.

The API surface stays small; the richness lives in the result, not the verbs:

Session::local() / ::ssh(cfg) / ::docker(container)        -> Session
session.exec(command)                                      -> ExecResult
session.exec_budgeted(command, &budget)                    -> ExecResult
session.exec_with_timeout(command, Option<&budget>, dur)   -> ExecResult
session.checkpoint(label?) / restore(id) / restore_last()  -> CheckpointId / restore report

Runnable examples live in the repository:

cargo run --example local
EXECKIT_SSH="user:password@host:22" cargo run --example ssh

Full API docs are on docs.rs/execkit.