Embedding and Machine-Readable Runs
Krate's wedge is safe execution of software that programs — including AI
agents — produce and run on a user's behalf. That needs two surfaces beyond
the interactive CLI: a library API other programs can embed, and a
machine-readable form of krate run.
The embedding API
krate_runtime::embed runs a component inside the capability sandbox with
no terminal: grants are supplied programmatically through SessionPolicy,
nothing prompts, and the app's stdout comes back captured next to a
classified exit.
use krate_policy::SessionPolicy; use krate_runtime::{embed, Config}; fn main() -> Result<(), Box<dyn std::error::Error>> { let component = std::fs::read("app.wasm")?; let mut config = Config::default(); config.session_policy = SessionPolicy::from_cli_grants(&[ "fs.read:./data/**".to_string(), ])?; let outcome = embed::run_component(&component, &config)?; println!("class: {}", outcome.exit_class().as_str()); println!("stdout: {}", outcome.stdout_lossy()); Ok(()) }
EmbedOutcome reports the exit code, an EmbedExitClass
(success, permission-denied, app-error, limit-exceeded), the captured
stdout bytes, and the run duration. Runtime-level failures (invalid
component, trap) surface as errors; a capability denial inside the app is a
classified outcome, not an error — policy-aware callers decide what to do
with it.
Grants never come from prompts here. SessionPolicy::from_cli_grants parses
explicit capability strings, and SessionPolicy::allow_all_declared grants
everything a manifest declares — the embedding caller owns that decision the
way a human owns the terminal prompt.
krate run --json (schema krate.run.v1)
With --json, the CLI prints exactly one JSON object on stdout describing
the run, and the app's own stdout is captured into that object instead of
streaming. Process exit codes stay identical to the normal mode, so existing
scripts keep working.
{
"schema": "krate.run.v1",
"app": {
"id": "dev.krate.clock",
"name": "krate-clock",
"version": "0.1.0-dev",
"world": "krate:app/cli@0.1.0"
},
"capabilities": {
"granted": ["io.stdout", "time.clock"],
"denied": []
},
"exit": {
"code": 0,
"class": "success",
"message": null
},
"duration_ms": 87,
"stdout": "app=krate-clock\n..."
}
Field notes:
appisnullwhen no manifest was provided.capabilities.grantedlists the effective session grants, with boundaries (fs.read:data/**,net.connect:host:port).capabilities.deniedis non-empty only when required capabilities were missing and the run was refused before the component started; the process exits5in that case, matching the interactive flow.exit.classis one ofsuccess,permission-denied(app exit code 5 by Krate convention, or a refusal before the run),app-error,limit-exceeded,invalid-component, ortrap.exit.codeisnullwhen the runtime stopped the component (limit-exceeded,invalid-component,trap).duration_msisnullwhen the run was refused before starting.stdoutholds the app's captured stdout as lossy UTF-8.
Current status
Done now:
Runtime::run_bytes_captured/run_file_capturedcapture app stdout for embedding callers.krate_runtime::embed::run_componentwithEmbedOutcomeandEmbedExitClass, doc-tested.krate run --jsonemittingkrate.run.v1, covered by CLI integration tests for the success, denied-before-run, and invalid-component paths.
The MCP server
krate-mcp-server (a crates/tools binary) exposes one MCP tool,
run_component, over newline-delimited JSON-RPC on stdio. Any MCP-capable
agent framework can execute components inside the sandbox without linking
Rust:
cargo build -p krate-tools --bin krate-mcp-server
claude mcp add krate -- target/debug/krate-mcp-server
The tool takes component_path, optional manifest_path, grants,
auto_grant, app_args, and sandbox_root, and returns the
krate.run.v1 report as the tool result (with isError set for anything
but success). Denials surface as data: calling without grants returns
permission-denied plus the exact missing capabilities, and the same call
with auto_grant succeeds — the agent observes the capability wall
directly.
Still pending on this track:
- richer per-call deny logs inside a run (today the denial signal is the app's own exit code plus the refusal-before-run path).