dart_monty

Sandboxed Python for Dart & Flutter.
Same code. Every platform.

dart pub add dart_monty
The Holy Stomping — Monty Python foot with Dart, Python, Rust, WASM, and JS logos holy stompin'
"And you may ask yourself—well, how did I get here?" — Talking Heads, "Once in a Lifetime"
8
Dart packages
6
platforms
11
Python ladder tiers
90%+
test coverage
0
Flutter imports required
The Why
You may ask yourself: well, how did I get here?

LLMs write Python. Your app runs on Dart. That gap kills you. Every agent framework, every code-generating model, every automation pipeline produces Python—and your Flutter app can't run it. Until now.

AI Agents

LLMs generate Python. Your app should execute it.

When an LLM writes code to answer a question, transform data, or call a tool—it writes Python. dart_monty lets your Dart and Flutter apps execute that code directly, in-process, without a server round-trip. The LLM writes 35 lines of glue. Monty runs it. Your host functions provide the domain logic. The agent loop closes locally.

Sandboxing

User-submitted code without the terror.

You want users to write scripts, formulas, automations—but you can't let arbitrary code touch the filesystem, the network, or memory you don't control. Monty's Python subset has no import, no eval(), no I/O. The sandbox isn't a policy layer bolted on top. It's the interpreter itself. There is no escape because there is nothing to escape to.

One Codebase, Proven Parity

Write it once. Know it works the same everywhere.

Most cross-platform stories are "write once, hope for the best." dart_monty has an 11-tier test suite that runs the same JSON fixtures on native FFI and browser WASM, diffs the JSONL output, and fails CI on any divergence. Parity isn't a goal. It's a gate. You don't ship if native and web disagree.

"The SubGenius must have Slack, or the whole world will be destroyed." — J.R. "Bob" Dobbs, The Book of the SubGenius

The What
A Rust Python interpreter. In your Dart and Flutter apps. Everywhere.

dart_monty wraps Monty—Pydantic's tree-walking Python interpreter written in Rust—in pure Dart bindings. Native desktop uses FFI. Web uses WASM in a Worker. Mobile gets Isolate-based execution. Your code doesn't change. The platform does. We maintain a fork with patches for cancellation and FFI safety until they land upstream.

pure dart

Zero Flutter Dependency

The entire platform interface is pure Dart. Use it in CLI tools, servers, or Flutter apps. No widget tree required.

sandboxed

Sandboxed by Construction

No file I/O. No network. No eval(). No import. Monty executes a Python subset that can't escape its sandbox. The interpreter IS the security model.

federated

Compile-Time Backend Selection

Conditional imports choose FFI or WASM at compile time. Zero runtime overhead. Zero platform checks. Monty() just works.

extensible

Host Function Plugins

Python calls your Dart functions. Execution pauses, your host resolves the call, execution resumes. Full async bridge with typed schemas.

resilient

Sealed Error Hierarchy

Six error subtypes. Exhaustive pattern matching. Rust panics caught at the FFI boundary, never crashing your app. Every failure path is typed.

persistent

Snapshot / Restore

Freeze execution state. Serialize it. Restore it later. Stateful sessions that survive app restarts, powered by Monty's snapshot protocol.

"Pull the wool over your own eyes." — The Church of the SubGenius

Architecture
This is not my beautiful interpreter.

Four packages form the core. A platform interface defines the contract. Two backends implement it. A bridge layer adds plugins and streaming. Everything is pure Dart.

graph TD
    subgraph APP["Your Application"]
        A["dart_monty
App-facing API"] end subgraph CORE["Platform Contract"] PI["dart_monty_platform_interface
Pure Dart • MontyPlatform • SPI"] end subgraph BACKENDS["Backend Implementations"] FFI["dart_monty_ffi
dart:ffi → Rust shared lib"] WASM["dart_monty_wasm
dart:js_interop → Web Worker"] end subgraph BRIDGE["Orchestration"] BR["dart_monty_bridge
Plugins • Streaming • Events"] end subgraph WRAPPERS["Platform Wrappers"] NAT["dart_monty_native
Isolate-based"] WEB["dart_monty_web
pub.dev web plugin"] end subgraph TOOLS["Developer Tools"] CLI["monty_cli
CLI tool"] MCP["dart_monty_mcp
MCP server"] end A --> PI A --> BR FFI --> PI WASM --> PI BR --> PI NAT --> FFI WEB --> WASM CLI --> A MCP --> BR style APP fill:#1a1a2e,stroke:#569cd6,stroke-width:2px style CORE fill:#1a1a2e,stroke:#c586c0,stroke-width:2px style BACKENDS fill:#1a1a2e,stroke:#6a9955,stroke-width:2px style BRIDGE fill:#1a1a2e,stroke:#dcdcaa,stroke-width:2px style WRAPPERS fill:#1a1a2e,stroke:#4ec9b0,stroke-width:1px style TOOLS fill:#1a1a2e,stroke:#808080,stroke-width:1px
Package dependency graph — arrows point from consumer to provider
Native Path (Desktop / Mobile)
graph LR
    D["Dart App"] --> MI["MontyNative
Isolate"] MI --> MF["MontyFfi
dart:ffi"] MF --> LIB["libdart_monty
.dylib / .so / .dll"] LIB --> MONTY["Monty
Rust interpreter"] style D fill:#1a1a2e,stroke:#569cd6 style MI fill:#1a1a2e,stroke:#4ec9b0 style MF fill:#1a1a2e,stroke:#6a9955 style LIB fill:#1a1a2e,stroke:#dcdcaa style MONTY fill:#1a1a2e,stroke:#c586c0,stroke-width:2px

FFI calls are synchronous. The Isolate wrapper runs them off the main thread—your UI never freezes, even on 500ms Python executions.

Web Path (Browser)
graph LR
    D["Dart
compiled to JS"] --> JS["js_interop
monty_glue.js"] JS --> W["Web Worker"] W --> WASM["Monty WASM
wasm32-wasip1"] style D fill:#1a1a2e,stroke:#569cd6 style JS fill:#1a1a2e,stroke:#dcdcaa style W fill:#1a1a2e,stroke:#4ec9b0 style WASM fill:#1a1a2e,stroke:#c586c0,stroke-width:2px

Chrome's 8MB sync compile limit means WASM must live in a Worker. Each session gets its own Worker—preemptive kill via Worker.terminate().

"Are we not men? We are Devo!" — Devo, "Jocko Homo"

Lifecycle
Stop making sense. Start making state machines.

Every Monty session follows a deterministic state machine. MontyStateMixin enforces valid transitions at the type level. No invalid states. No race conditions. No surprises.

stateDiagram-v2
    [*] --> Idle : create()

    Idle --> Idle : run() — one-shot execution
    Idle --> Active : start() — iterative execution

    Active --> Active : resume() — pending result
    Active --> Idle : resume() — complete result

    Active --> Idle : cancel()

    Idle --> Disposed : dispose()
    Active --> Disposed : dispose()

    Disposed --> [*]

    note right of Active
        Python pauses on host function call.
        Dart resolves. Execution resumes.
    end note
    
MontyPlatform lifecycle — guarded by MontyStateMixin
graph TD
    ME["MontyError
sealed class"] ME --> MSE["MontyScriptError
Python raised an exception"] ME --> MCE["MontyCancelledError
Cooperative cancellation"] ME --> MRE["MontyResourceError
Limits exceeded"] ME --> MPE["MontyPanicError
Rust panic caught at FFI"] ME --> MCRE["MontyCrashError
Unexpected crash"] ME --> MDE["MontyDisposedError
Used after dispose"] style ME fill:#1a1a2e,stroke:#f44747,stroke-width:2px style MSE fill:#1a1a2e,stroke:#dcdcaa style MCE fill:#1a1a2e,stroke:#c586c0 style MRE fill:#1a1a2e,stroke:#ce9178 style MPE fill:#1a1a2e,stroke:#f44747 style MCRE fill:#1a1a2e,stroke:#f44747 style MDE fill:#1a1a2e,stroke:#808080
Sealed error hierarchy — exhaustive switch, no surprises
"All statements are true in some sense, false in some sense, meaningless in some sense, true and false in some sense, true and meaningless in some sense, false and meaningless in some sense, and true and false and meaningless in some sense." — Principia Discordia, The Fifth Commandment

Parity Verification
The Python Ladder

11 tiers of JSON test fixtures. Every tier runs on native FFI AND WASM. Results are diffed as JSONL. Zero divergences on logic. This is how you prove cross-platform parity—not with hope, but with evidence.

Expressions

1 + 2, "hello", arithmetic, string ops

Variables

x = 42, assignment, lookup, scope

Control Flow

if, while, break, continue

Functions

def greet(name):, closures, recursion

Errors

NameError, TypeError, ZeroDivisionError

External Functions

Host function calls, pause/resume protocol

Keyword Args

greet(name="world"), positional + keyword

Exceptions

try/except/finally, traceback

Async

External functions resolving as futures

Script Naming

__name__, source attribution, metadata

Edge Cases

Unicode, empty programs, max recursion, limits

"Eternal Salvation or Triple Your Money Back." — The Church of the SubGenius

Deep Dive
Features that actually matter.
Panic Safety

Burning down the house? Not today.

Every Rust FFI function is wrapped in ffi_progress!—a macro that catches panics via std::panic::catch_unwind, checks null pointers, and writes errors to caller-provided buffers. Rust panics become typed Dart errors, never process crashes. This is the pattern that should be standard for any Dart-Rust embedding.

native/src/lib.rs
// Every FFI entry point. Every time.
ffi_progress!(handle, out_error, |h| {
    // Null check on handle pointer
    // Panic containment via catch_unwind
    // Error written to out-parameter
    h.process_progress(code, external_fns)
});
Cancellation

When a problem comes along—you must whip it.

MontyCancelToken wraps an Arc<AtomicBool> checked in Monty's bytecode loop. Cooperative cancellation without preemption. On web, Worker.terminate() gives you true preemptive kill. Zombie tracking handles FFI calls that outlive their timeout.

cancellation.dart
final token = await monty.cancelToken();

// From another isolate or timer:
await monty.cancel(token);

// Result is MontyCancelledError
switch (error) {
  MontyCancelledError() => print('whipped it'),
  MontyScriptError(:final exception) => handleErr(exception),
}
Plugins

Freedom of choice is what you got.

Group host functions into namespaced plugins. The registry validates collisions at registration time, not at runtime. Python code can call list_functions() and help("fn_name") to discover what's available. Introspection is auto-generated.

weather_plugin.dart
class WeatherPlugin extends MontyPlugin {
  String get namespace => 'weather';

  List<HostFunction> get functions => [
    HostFunction(
      schema: const HostFunctionSchema(
        name: 'get_forecast',
        params: [HostParam(name: 'city')],
      ),
      handler: (args) async => fetchWeather(args['city']),
    ),
  ];
}
Iterative Execution

Same as it ever was. Same as it ever was.

start() begins execution. When Python hits a host function call, execution pauses—returning a MontyPending with the function name and arguments. Your Dart code resolves it. resume() continues from exactly where it stopped. This loop is the beating heart of dart_monty.

sequenceDiagram
    participant App as Dart App
    participant M as Monty
    participant PY as Python Code

    App->>M: start(code)
    M->>PY: execute
    PY->>M: call host_fn("get_data", args)
    M-->>App: MontyPending{name, args}
    Note over App: resolve async
    App->>M: resume(result)
    M->>PY: inject result, continue
    PY->>M: return final_value
    M-->>App: MontyComplete{value}
      
"We must understand the NATURE OF SLACK!
We must achieve SLACK or the whole world will be destroyed!" — J.R. "Bob" Dobbs

The Boundary
17 C functions. Every one panic-safe.

The native Rust crate exposes a minimal C ABI surface. Four source files. A state machine with 7 handle variants. Bidirectional JSON serialization for all values. No leaked pointers. No unwound panics.

graph LR
    subgraph DART["Dart Side"]
        NB["NativeBindingsFfi
dart:ffi pointers"] end subgraph FFI["C ABI Boundary"] direction TB CREATE["monty_create()"] RUN["monty_run()"] START["monty_start()"] RESUME["monty_resume()"] SNAP["monty_snapshot()"] RESTORE["monty_restore()"] CANCEL["monty_cancel()"] FREE["monty_free()"] end subgraph RUST["Rust Side"] HANDLE["MontyHandle
state machine"] PROC["process_progress()
generic dispatch"] CONV["convert.rs
MontyObject ↔ JSON"] ERR["error.rs
catch_unwind"] end NB --> CREATE NB --> RUN NB --> START NB --> RESUME NB --> SNAP NB --> CANCEL NB --> FREE CREATE --> HANDLE RUN --> PROC START --> PROC RESUME --> PROC PROC --> CONV PROC --> ERR style DART fill:#1a1a2e,stroke:#569cd6,stroke-width:2px style FFI fill:#1a1a2e,stroke:#dcdcaa,stroke-width:2px style RUST fill:#1a1a2e,stroke:#c586c0,stroke-width:2px
The FFI boundary — 17 extern "C" functions wrapping Monty's Rust interpreter
"It's not true unless it makes you laugh, but you don't understand it until it makes you weep." — Robert Anton Wilson, The Illuminatus! Trilogy

Contract Design
Two barrels. One contract. No confusion.

The platform interface exports two barrel files: the public API for apps, and the SPI for backend implementers. This separation prevents accidental coupling between app code and implementation details.

graph TB
    subgraph PUBLIC["dart_monty_platform_interface.dart
Public API — apps import this"] MP["MontyPlatform"] MR["MontyResult"] MERR["MontyError (sealed)"] MS["MontySession"] ML["MontyLimits"] MCT["MontyCancelToken"] end subgraph SPI["monty_backend_spi.dart
SPI — backends only"] BMP["BaseMontyPlatform"] MCB["MontyCoreBindings"] MSM["MontyStateMixin"] MSC["MontySnapshotCapable"] MFC["MontyFutureCapable"] end FFI_PKG["dart_monty_ffi"] --> SPI WASM_PKG["dart_monty_wasm"] --> SPI APP["Your App"] --> PUBLIC BRIDGE["dart_monty_bridge"] --> PUBLIC APP -.-x SPI BRIDGE -.-x SPI style PUBLIC fill:#1a1a2e,stroke:#569cd6,stroke-width:2px style SPI fill:#1a1a2e,stroke:#c586c0,stroke-width:2px style APP fill:#1a1a2e,stroke:#6a9955 style FFI_PKG fill:#1a1a2e,stroke:#6a9955 style WASM_PKG fill:#1a1a2e,stroke:#6a9955 style BRIDGE fill:#1a1a2e,stroke:#dcdcaa
Apps never import the SPI. Backends never skip it. The dashed X means "must not import."
"Convictions cause convicts." — Principia Discordia

Plugin System
Beautiful world. Beautiful plugins.

The bridge layer wraps the raw start/resume loop into a streaming event pipeline. Plugins register host functions. The registry auto-generates list_functions() and help() for runtime introspection.

graph TB
    subgraph BRIDGE["DefaultMontyBridge"]
        LOOP["Start / Resume Loop"]
        STREAM["Stream<BridgeEvent>"]
    end

    subgraph REGISTRY["PluginRegistry"]
        REG["register()"]
        VALID["Collision detection"]
        INTRO["Auto introspection"]
    end

    subgraph PLUGINS["Your Plugins"]
        P1["WeatherPlugin
weather.get_forecast"] P2["StockPlugin
stock.get_price"] P3["MathPlugin
math.solve"] end subgraph EVENTS["BridgeEvent (sealed)"] TC["BridgeToolCallResult"] TX["BridgeTextContent"] FIN["BridgeRunFinished"] end P1 --> REG P2 --> REG P3 --> REG REG --> VALID VALID --> INTRO REGISTRY --> BRIDGE LOOP --> STREAM STREAM --> TC STREAM --> TX STREAM --> FIN style BRIDGE fill:#1a1a2e,stroke:#dcdcaa,stroke-width:2px style REGISTRY fill:#1a1a2e,stroke:#c586c0,stroke-width:2px style PLUGINS fill:#1a1a2e,stroke:#6a9955,stroke-width:2px style EVENTS fill:#1a1a2e,stroke:#569cd6,stroke-width:2px
Plugin registry → bridge dispatch → event stream
Bridge Middleware

Cross-cutting concerns at the chokepoint.

BridgeMiddleware intercepts every tool call through the bridge—grounding, telemetry, rate limiting, access control—without wrapping individual plugins. A sealed CallRole hierarchy distinguishes orchestration infrastructure from agent-initiated tool calls, so middleware can enforce policy selectively.

graph LR
    PY["Python seed"] -->|"__role__=infra"| MW1["Grounding"]
    PY -->|"__role__=tool"| MW1
    MW1 --> MW2["Telemetry"]
    MW2 --> MW3["Rate Limit"]
    MW3 --> H["Plugin Handler"]

    style PY fill:#1a1a2e,stroke:#569cd6,stroke-width:2px
    style MW1 fill:#1a1a2e,stroke:#c586c0,stroke-width:2px
    style MW2 fill:#1a1a2e,stroke:#c586c0,stroke-width:2px
    style MW3 fill:#1a1a2e,stroke:#c586c0,stroke-width:2px
    style H fill:#1a1a2e,stroke:#6a9955,stroke-width:2px
      
"Freedom of choice is what you got. Freedom from choice is what you want." — Devo, "Freedom of Choice"

Try It Now
Python. In your browser. Right now.

These demos compile Dart to JavaScript, load the Monty WASM binary in a Web Worker, and execute Python—all client-side. No server. No backend. Just your browser and a Rust interpreter compiled to WebAssembly.

WASM

Python Executor

Run Python expressions and functions through Monty WASM. See results in real time. Open DevTools for the full story.

PARITY

Python Ladder

Run all 11 tiers of cross-platform test fixtures in-browser. Watch every tier pass. This is the parity guarantee, live.

INTERACTIVE

Algorithm Visualizer

8 sorting algorithms. Python writes the logic. Monty's iterative execution pauses at each step. You see every comparison and swap.

MCP

MCP Server Docs

Give your LLM a Python interpreter. Model Context Protocol server with host function plugins, session persistence, and 6 guides.

FLUTTER

Flutter Web App

Full Material app with sorting visualizer, traveling salesman, ladder runner, and code examples. May not be available in all builds.


Get Started
Three lines. That's it.
main.dart
import 'package:dart_monty/dart_monty.dart';

void main() async {
  final monty = Monty();
  final result = await monty.run('2 + 2');
  print(result.value); // 4
}
stateful_session.dart
final session = MontySession(platform: Monty());

await session.run('x = 42');
await session.run('y = x * 2');
final result = await session.run('x + y');
print(result.value); // 126
with_plugins.dart
final bridge = DefaultMontyBridge(platform: Monty());
final registry = PluginRegistry()
  ..register(WeatherPlugin())
  ..register(StockPlugin());
await registry.attachTo(bridge);

// Python can now call weather.get_forecast("NYC")
// and stock.get_price("AAPL")
await for (final event in bridge.run(script)) {
  // handle BridgeEvent stream
}
"It's a beautiful world we live in. A sweet romantic place." — Devo, "Beautiful World"

Self-Evolving
The road to somewhere.

dart_monty is not a finished product. It's an evolving system. The Python ladder grows. The plugin API deepens. The WASM path optimizes. Supervision trees emerge. Every commit makes the parity guarantee stronger. Every test fixture is evidence.

timeline
    title dart_monty Evolution
    section Foundation
        Pure Dart platform interface : MontyPlatform contract
        Native FFI bindings : dart_monty_ffi + Rust crate
        WASM Web Worker : dart_monty_wasm + JS bridge
    section Parity
        Python Ladder : 11-tier fixture suite
        Cross-path JSONL diff : Native vs WASM verification
        Snapshot portability : State persistence probes
    section Safety
        ffi_progress! macro : Panic containment
        Sealed MontyError : 6 typed subtypes
        CancellableTracker : Cooperative cancellation
    section Orchestration
        Plugin Registry : Namespaced host functions
        Bridge Events : Streaming execution
        Bridge Middleware : Sealed CallRole + onion chain
        MCP Server : LLM integration
    section Horizon
        Supervision Trees : Fault tolerance
        WASM Optimization : 16MB per session target
        REPL Mode : Stateful multi-step
        Rich Types : Tuples, sets, dataclasses
    
Past, present, and horizon — every phase builds on the last
"Letting the days go by, let the water hold me down.
Letting the days go by, water flowing underground.
Into the blue again, after the money's gone.
Once in a lifetime, water flowing underground." — Talking Heads, "Once in a Lifetime"
"The border between the Real and the Unreal is not fixed, but just marks the last place where rival gangs of programmers managed to push the line." — Robert Anton Wilson, The Illuminatus! Trilogy