You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/23 05:58:15 UTC

(maka) 02/04: feat(code-mode): add isolated Monty Python cell worker

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/python-code-mode
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 127926337e5575b2aec8250136d6972b4caa0e18
Author: Wang <m4...@gmail.com>
AuthorDate: Fri Oct 23 13:29:25 2026 +0800

    feat(code-mode): add isolated Monty Python cell worker
    
    Generated-by: Codex
---
 Cargo.lock                                | 605 +++++++++++++++++++++++++++++-
 Cargo.toml                                |   2 +
 platform/code-runtime/src/cell_context.rs |   2 +-
 platform/python-runtime/Cargo.toml        |  40 ++
 platform/python-runtime/src/helpers.rs    | 162 ++++++++
 platform/python-runtime/src/lib.rs        | 394 +++++++++++++++++++
 platform/python-runtime/src/main.rs       |  28 ++
 platform/python-runtime/src/protocol.rs   |  67 ++++
 platform/python-runtime/src/value.rs      |  91 +++++
 platform/python-runtime/src/worker.rs     | 287 ++++++++++++++
 platform/python-runtime/tests/runtime.rs  | 413 ++++++++++++++++++++
 11 files changed, 2072 insertions(+), 19 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 34f653a0cd..4a654dc5f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -406,6 +406,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "ar_archive_writer"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+dependencies = [
+ "object 0.37.3",
+]
+
 [[package]]
 name = "arbitrary"
 version = "1.4.2"
@@ -845,6 +854,35 @@ dependencies = [
  "zbus",
 ]
 
+[[package]]
+name = "attribute-derive"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f9ef7a68684657ce9da341c934c73b63df7a3585a61e366d84c84ed5d3d167"
+dependencies = [
+ "attribute-derive-macro",
+ "manyhow",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "attribute-derive-macro"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300d426e5e73549125bb8e4649bdd5508f82c94ddf898fb0517246e71dfb8e98"
+dependencies = [
+ "collection_literals",
+ "interpolator",
+ "manyhow",
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "quote-use",
+ "syn 3.0.5",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.5.1"
@@ -1625,6 +1663,18 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "char_str"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9cba78a5abb515610e10af297615434b019755b5d7be14e559ff5340dff50c4"
+dependencies = [
+ "castaway",
+ "get-size2",
+ "itoa",
+ "ryu",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.45"
@@ -1877,6 +1927,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "collection_literals"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -3256,6 +3312,12 @@ version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
 
+[[package]]
+name = "drop_bomb"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
+
 [[package]]
 name = "dtoa"
 version = "1.0.11"
@@ -3657,6 +3719,17 @@ dependencies = [
  "regex-syntax",
 ]
 
+[[package]]
+name = "fancy-regex"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
+dependencies = [
+ "bit-set 0.8.0",
+ "regex-automata",
+ "regex-syntax",
+]
+
 [[package]]
 name = "fancy-regex"
 version = "0.18.0"
@@ -4205,6 +4278,31 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "get-size-derive2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28f0daa179069133df60523e0f59d0a6bd2d3db9ccae01b630896d269c4a802e"
+dependencies = [
+ "attribute-derive",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "get-size2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050a5d07359ebfac7f0b74916d94cfa30575e45fdc7e9bfbb0f76d5d4203325a"
+dependencies = [
+ "compact_str 0.10.0",
+ "get-size-derive2",
+ "hashbrown 0.17.1",
+ "ordermap",
+ "smallvec",
+ "thin-vec",
+]
+
 [[package]]
 name = "gethostname"
 version = "1.1.0"
@@ -4215,6 +4313,15 @@ dependencies = [
  "windows-link 0.2.1",
 ]
 
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.17"
@@ -5217,7 +5324,7 @@ dependencies = [
  "log",
  "presser",
  "thiserror 2.0.20",
- "windows 0.59.0",
+ "windows 0.62.2",
 ]
 
 [[package]]
@@ -6746,7 +6853,7 @@ dependencies = [
  "libc",
  "percent-encoding",
  "pin-project-lite",
- "socket2 0.5.10",
+ "socket2",
  "tokio",
  "tower-service",
  "tracing",
@@ -6764,7 +6871,7 @@ dependencies = [
  "js-sys",
  "log",
  "wasm-bindgen",
- "windows-core 0.59.0",
+ "windows-core 0.62.2",
 ]
 
 [[package]]
@@ -7152,6 +7259,12 @@ dependencies = [
  "syn 2.0.119",
 ]
 
+[[package]]
+name = "interpolator"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+
 [[package]]
 name = "inventory"
 version = "0.3.24"
@@ -7208,6 +7321,18 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "is-macro"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8267aa6001e25494f3015f9663bbd88a18240c74483afa5f0934a1b3e4c388e9"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
 [[package]]
 name = "is-wsl"
 version = "0.4.0"
@@ -7320,6 +7445,21 @@ dependencies = [
  "jiff-tzdb",
 ]
 
+[[package]]
+name = "jiter"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc2f2b4e673d798b3dbad0af24bc23b92013e680a6b9ad300b4e5e52fafe0586"
+dependencies = [
+ "ahash 0.8.12",
+ "bitvec",
+ "lexical-parse-float",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "pyo3",
+ "smallvec",
+]
+
 [[package]]
 name = "jni"
 version = "0.22.4"
@@ -7643,6 +7783,31 @@ version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
 
+[[package]]
+name = "lexical-parse-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-util"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17"
+
 [[package]]
 name = "libc"
 version = "0.2.189"
@@ -9146,6 +9311,23 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "maka-python-runtime"
+version = "0.2.0"
+dependencies = [
+ "futures-util",
+ "maka-agent-facts",
+ "maka-code-runtime",
+ "monty",
+ "monty-alloc",
+ "monty-types",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-util",
+ "uuid",
+]
+
 [[package]]
 name = "maka-responses"
 version = "0.2.0"
@@ -9633,6 +9815,29 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "manyhow"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1704b073316272a6785b70599f91f73b49022c2e290b065591ee6b9109ed8c7"
+dependencies = [
+ "manyhow-macros",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "manyhow-macros"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31601db71f1f5fb6f831a18aa3052d13a62528ee4f2cba7e8c9a5e258d953725"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "markdown"
 version = "1.0.0"
@@ -9796,6 +10001,22 @@ dependencies = [
  "unicase",
 ]
 
+[[package]]
+name = "minicbor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7244eafd1e09e7c4e33043881997aa6ab6dffff3ed2cee3395a278980481d6e"
+
+[[package]]
+name = "minicbor-serde"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05ae1ca1d2c55196b389c8308119aa0e2aa8d31115f018dcc23ba3c5d350eb3"
+dependencies = [
+ "minicbor",
+ "serde",
+]
+
 [[package]]
 name = "minimal-lexical"
 version = "0.2.1"
@@ -9843,6 +10064,87 @@ dependencies = [
  "windows-sys 0.61.2",
 ]
 
+[[package]]
+name = "monty"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d0f5559d7a445c22ed296fe8252aa067c80b7eb2bc70aeae341cf9967e58012"
+dependencies = [
+ "ahash 0.8.12",
+ "chrono",
+ "fancy-regex 0.17.0",
+ "getrandom 0.3.4",
+ "hashbrown 0.16.1",
+ "indexmap 2.14.2",
+ "itertools 0.14.0",
+ "itoa",
+ "jiter",
+ "libm",
+ "memchr",
+ "minicbor-serde",
+ "monty-macros",
+ "monty-types",
+ "num-bigint 0.4.8",
+ "num-integer",
+ "num-traits",
+ "postcard",
+ "ruff_python_ast",
+ "ruff_python_codegen",
+ "ruff_python_parser",
+ "ruff_python_stdlib",
+ "ruff_source_file",
+ "ruff_text_size",
+ "serde",
+ "serde_bytes",
+ "sha1 0.11.0",
+ "sha2 0.11.1",
+ "smallvec",
+ "speedate",
+ "strum 0.27.2",
+ "unicode-general-category",
+ "unicode-normalization",
+ "unicode_names2",
+]
+
+[[package]]
+name = "monty-alloc"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f9d819abd1c64c36dbfb05518a8d1ba28c55f38eb802a9c0c58818dee3f6d4"
+dependencies = [
+ "monty-types",
+]
+
+[[package]]
+name = "monty-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbd7672e36311f10645e3a9e6776b643711543ec3fb415546ffde751e6d53003"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "monty-types"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786b179998d4799dc96a5c25e91e3774a1fa8dd6cca441ddeedc9c146fd9a8cd"
+dependencies = [
+ "chrono",
+ "jiff",
+ "monty-macros",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "ruff_python_stdlib",
+ "serde",
+ "serde_bytes",
+ "strum 0.27.2",
+ "unicode-general-category",
+ "web-time",
+]
+
 [[package]]
 name = "moxcms"
 version = "0.8.1"
@@ -10339,6 +10641,7 @@ checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
 dependencies = [
  "num-integer",
  "num-traits",
+ "serde",
 ]
 
 [[package]]
@@ -10970,6 +11273,15 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "ordermap"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16a756ddcdeb6c0efc8927bdf0ed112885815a35ba350472a467be5104f01ca1"
+dependencies = [
+ "indexmap 2.14.2",
+]
+
 [[package]]
 name = "os_pipe"
 version = "1.2.1"
@@ -11935,7 +12247,7 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2 0.10.9",
- "socket2 0.6.5",
+ "socket2",
  "thiserror 1.0.69",
  "tiny-skia 0.11.4",
  "tokio",
@@ -12245,6 +12557,17 @@ dependencies = [
  "toml_edit 0.25.15+spec-1.1.0",
 ]
 
+[[package]]
+name = "proc-macro-utils"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "smallvec",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.107"
@@ -12314,6 +12637,16 @@ dependencies = [
  "syn 2.0.119",
 ]
 
+[[package]]
+name = "psm"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
 [[package]]
 name = "ptr_meta"
 version = "0.1.4"
@@ -12363,6 +12696,65 @@ version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
 
+[[package]]
+name = "pyo3"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
+dependencies = [
+ "libc",
+ "num-bigint 0.4.8",
+ "num-traits",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
+dependencies = [
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
 [[package]]
 name = "qoi"
 version = "0.4.1"
@@ -12623,7 +13015,7 @@ dependencies = [
  "quinn-udp",
  "rustc-hash 2.1.3",
  "rustls",
- "socket2 0.5.10",
+ "socket2",
  "thiserror 2.0.20",
  "tokio",
  "tracing",
@@ -12662,7 +13054,7 @@ dependencies = [
  "cfg_aliases",
  "libc",
  "once_cell",
- "socket2 0.5.10",
+ "socket2",
  "tracing",
  "windows-sys 0.61.2",
 ]
@@ -12676,6 +13068,28 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "quote-use"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48356e493823dc3c1d7e9fc7b3ff204ecec48afbdd718fc4134091b88eb63f3d"
+dependencies = [
+ "quote",
+ "quote-use-macros",
+]
+
+[[package]]
+name = "quote-use-macros"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf1c5de6a7cc231c71e03c77aa4c7019097859ea7029156b54d2894a8430704"
+dependencies = [
+ "proc-macro-utils",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.5",
+]
+
 [[package]]
 name = "r-efi"
 version = "5.3.0"
@@ -13892,6 +14306,119 @@ dependencies = [
  "thiserror 2.0.20",
 ]
 
+[[package]]
+name = "ruff_python_ast"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aa1de604c478a790cfbcc9c1fef3060e619ed87dd776c6614862ed7e59dc461"
+dependencies = [
+ "aho-corasick",
+ "arrayvec",
+ "bitflags 2.13.2",
+ "char_str",
+ "compact_str 0.10.0",
+ "get-size2",
+ "is-macro",
+ "memchr",
+ "ruff_python_trivia",
+ "ruff_source_file",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "thin-vec",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "ruff_python_codegen"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61039f2c61869bdabf2e5448b561ab6546b787718926c78973ebfefe03f84298"
+dependencies = [
+ "ruff_python_ast",
+ "ruff_python_literal",
+ "ruff_python_parser",
+ "ruff_source_file",
+ "ruff_text_size",
+]
+
+[[package]]
+name = "ruff_python_literal"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b6025882ae43876fda9867693e14d1d749acefb3db2763d81bfa32a8cd92682"
+dependencies = [
+ "bitflags 2.13.2",
+ "icu_properties",
+ "itertools 0.15.0",
+ "ruff_python_ast",
+]
+
+[[package]]
+name = "ruff_python_parser"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b44b423a842d5d2f10807189a260ba677d4eacdc98c5d0459b66bef67fb78d97"
+dependencies = [
+ "bitflags 2.13.2",
+ "bstr",
+ "drop_bomb",
+ "get-size2",
+ "hashbrown 0.17.1",
+ "memchr",
+ "ruff_python_ast",
+ "ruff_python_trivia",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "stacker",
+ "static_assertions",
+ "thin-vec",
+ "unicode-ident",
+ "unicode-normalization",
+ "unicode_names2",
+]
+
+[[package]]
+name = "ruff_python_stdlib"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea8c1e6e69f7570e1f24c7a26ef93f6fd1475e2d76aa9ca4a78093c1f80523cb"
+dependencies = [
+ "bitflags 2.13.2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ruff_python_trivia"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990dadfad3aa3fd63040d98056112e5a377af8ada0c9c7659e8907a4b3c7cb34"
+dependencies = [
+ "itertools 0.15.0",
+ "ruff_source_file",
+ "ruff_text_size",
+ "rustc-hash 2.1.3",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ruff_source_file"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed11e9366de40e79707c0e575afd8beffad58ec94c253577d516b8679cf98308"
+dependencies = [
+ "memchr",
+ "ruff_text_size",
+]
+
+[[package]]
+name = "ruff_text_size"
+version = "0.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a4edc941f215495170a1bda0beeeb31db2fdcd67beecfe7f29d94b2663e1965"
+dependencies = [
+ "get-size2",
+]
+
 [[package]]
 name = "rusqlite"
 version = "0.39.0"
@@ -15007,16 +15534,6 @@ dependencies = [
  "serde_core",
 ]
 
-[[package]]
-name = "socket2"
-version = "0.5.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
-dependencies = [
- "libc",
- "windows-sys 0.52.0",
-]
-
 [[package]]
 name = "socket2"
 version = "0.6.5"
@@ -15045,6 +15562,17 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "speedate"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aba069c070b5e213f2a094deb7e5ed50ecb092be36102a4f4042e8d2056d060e"
+dependencies = [
+ "lexical-parse-float",
+ "strum 0.27.2",
+ "strum_macros 0.27.2",
+]
+
 [[package]]
 name = "spin"
 version = "0.9.9"
@@ -15283,6 +15811,19 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
 
+[[package]]
+name = "stacker"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "static_assertions"
 version = "1.1.0"
@@ -15630,7 +16171,7 @@ dependencies = [
  "ntapi",
  "objc2-core-foundation",
  "objc2-io-kit",
- "windows 0.59.0",
+ "windows 0.61.3",
 ]
 
 [[package]]
@@ -15855,6 +16396,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "thin-vec"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e"
+
 [[package]]
 name = "thiserror"
 version = "1.0.69"
@@ -16061,7 +16608,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.6.5",
+ "socket2",
  "tokio-macros",
  "tracing",
  "windows-sys 0.61.2",
@@ -16694,6 +17241,28 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
+[[package]]
+name = "unicode_names2"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd"
+dependencies = [
+ "phf 0.11.3",
+ "unicode_names2_generator",
+]
+
+[[package]]
+name = "unicode_names2_generator"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e"
+dependencies = [
+ "getopts",
+ "log",
+ "phf_codegen 0.11.3",
+ "rand 0.8.5",
+]
+
 [[package]]
 name = "uniffi"
 version = "0.31.0"
diff --git a/Cargo.toml b/Cargo.toml
index 3f563132b0..a12e993db3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,6 +56,7 @@ members = [
     "plugins/workspace/workspace-jev/engine",
     "platform/js-runtime",
     "platform/code-runtime",
+    "platform/python-runtime",
     "plugins/agent/model",
     "plugins/agent/provider-protocol",
     "platform/network",
@@ -90,6 +91,7 @@ license = "Apache-2.0"
 publish = false
 
 [workspace.dependencies]
+maka-python-runtime = { path = "platform/python-runtime" }
 hex = "0.4.3"
 maka-ui-authoring = { path = "plugins/application/authoring/native" }
 maka-app-client = { path = "products/maka/client" }
diff --git a/platform/code-runtime/src/cell_context.rs b/platform/code-runtime/src/cell_context.rs
index a7c8a25529..ba33178f1a 100644
--- a/platform/code-runtime/src/cell_context.rs
+++ b/platform/code-runtime/src/cell_context.rs
@@ -68,7 +68,7 @@ pub enum CellOutput {
     },
 }
 
-#[derive(Clone, Debug, Serialize)]
+#[derive(Clone, Debug, Deserialize, Serialize)]
 pub struct ToolMetadata {
     pub name: String,
     pub description: String,
diff --git a/platform/python-runtime/Cargo.toml b/platform/python-runtime/Cargo.toml
new file mode 100644
index 0000000000..c73625dabc
--- /dev/null
+++ b/platform/python-runtime/Cargo.toml
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "maka-python-runtime"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+publish.workspace = true
+
+[[bin]]
+name = "maka-python-worker"
+path = "src/main.rs"
+
+[dependencies]
+maka-agent-facts.workspace = true
+maka-code-runtime.workspace = true
+futures-util.workspace = true
+monty = "=1.3.0"
+monty-types = "=1.3.0"
+monty-alloc = { version = "=1.3.0", features = ["exit-code"] }
+serde.workspace = true
+serde_json.workspace = true
+tokio = { workspace = true, features = ["process"] }
+tokio-util.workspace = true
+uuid.workspace = true
diff --git a/platform/python-runtime/src/helpers.rs b/platform/python-runtime/src/helpers.rs
new file mode 100644
index 0000000000..8bce8aeb84
--- /dev/null
+++ b/platform/python-runtime/src/helpers.rs
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_code_runtime::{CellContext, CellDiagnostic, CellDiagnosticKind, CellOutput};
+use serde_json::{Value, json};
+
+pub(crate) fn invoke(
+    name: &str,
+    mut args: Vec<Value>,
+    mut kwargs: serde_json::Map<String, Value>,
+    context: &CellContext,
+) -> Result<Value, CellDiagnostic> {
+    let fields: &[&str] = match name {
+        "text" | "notify" | "audio" | "generated_image" => &["value"],
+        "image" => &["value", "detail"],
+        "store" => &["key", "value"],
+        "load" => &["key"],
+        "exit" | "yield_control" => &[],
+        _ => return Err(error("unknown Python helper")),
+    };
+    if args.len() > fields.len() {
+        return Err(error("too many helper arguments"));
+    }
+    for field in fields.iter().take(args.len()) {
+        if kwargs.contains_key(*field) {
+            return Err(error("argument supplied twice"));
+        }
+    }
+    for field in fields.iter().skip(args.len()) {
+        match kwargs.remove(*field) {
+            Some(value) => args.push(value),
+            None if name == "image" && *field == "detail" => args.push(Value::Null),
+            None => return Err(error(format!("missing argument: {field}"))),
+        }
+    }
+    if !kwargs.is_empty() {
+        return Err(error("unknown helper keyword argument"));
+    }
+    match name {
+        "text" => context.emit(CellOutput::Text {
+            text: display(&args[0]),
+        })?,
+        "notify" => context.notify(display(&args[0]))?,
+        "store" => context.store(key(&args[0])?.into(), args[1].clone())?,
+        "load" => return Ok(context.load(key(&args[0])?).unwrap_or(Value::Null)),
+        "yield_control" => context.yield_output(),
+        "exit" => {}
+        "image" => {
+            let value = &args[0];
+            let detail = if !args[1].is_null() {
+                args[1].clone()
+            } else {
+                value
+                    .get("detail")
+                    .or_else(|| value.pointer("/_meta/codex~1imageDetail"))
+                    .cloned()
+                    .unwrap_or(Value::Null)
+            };
+            if value.get("ref").is_some() {
+                emit(
+                    context,
+                    json!({"kind":"image","image":{"mimeType":value.get("mimeType"),"ref":value.get("ref"),"detail":detail}}),
+                )?;
+            } else {
+                media(
+                    context,
+                    "image",
+                    value.get("image_url").unwrap_or(value),
+                    detail,
+                )?;
+            }
+        }
+        "audio" => media(
+            context,
+            "audio",
+            args[0].get("audio_url").unwrap_or(&args[0]),
+            Value::Null,
+        )?,
+        "generated_image" => {
+            media(
+                context,
+                "image",
+                args[0]
+                    .get("image_url")
+                    .ok_or_else(|| error("missing image_url"))?,
+                Value::Null,
+            )?;
+            if let Some(hint) = args[0].get("output_hint").filter(|value| !value.is_null()) {
+                context.emit(CellOutput::Text {
+                    text: display(hint),
+                })?;
+            }
+        }
+        _ => unreachable!(),
+    }
+    Ok(Value::Null)
+}
+fn media(
+    context: &CellContext,
+    kind: &str,
+    value: &Value,
+    detail: Value,
+) -> Result<(), CellDiagnostic> {
+    let content = if let Some(url) = value.as_str() {
+        let (mime, data) = url
+            .strip_prefix("data:")
+            .and_then(|value| value.split_once(";base64,"))
+            .ok_or_else(|| error("expected a base64 data URL"))?;
+        if !mime.starts_with(&format!("{kind}/"))
+            || !data
+                .trim_end_matches('=')
+                .bytes()
+                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
+            || data.len() - data.trim_end_matches('=').len() > 2
+        {
+            return Err(error("expected a base64 data URL"));
+        }
+        json!({"type":kind,"mimeType":mime,"data":data})
+    } else {
+        if value.get("type").and_then(Value::as_str) != Some(kind) {
+            return Err(error(format!("expected a {kind} content block")));
+        }
+        json!({"type":kind,"mimeType":value.get("mimeType"),"data":value.get("data")})
+    };
+    emit(
+        context,
+        json!({"kind":"media","content":content,"detail":detail}),
+    )
+}
+fn emit(context: &CellContext, value: Value) -> Result<(), CellDiagnostic> {
+    context.emit(serde_json::from_value(value).map_err(error)?)
+}
+fn key(value: &Value) -> Result<&str, CellDiagnostic> {
+    value
+        .as_str()
+        .ok_or_else(|| error("store key must be a string"))
+}
+fn display(value: &Value) -> String {
+    value
+        .as_str()
+        .map(str::to_owned)
+        .unwrap_or_else(|| value.to_string())
+}
+pub(crate) fn error(message: impl std::fmt::Display) -> CellDiagnostic {
+    CellDiagnostic::new(CellDiagnosticKind::ExecutionError, message.to_string())
+}
diff --git a/platform/python-runtime/src/lib.rs b/platform/python-runtime/src/lib.rs
new file mode 100644
index 0000000000..0deb74cf37
--- /dev/null
+++ b/platform/python-runtime/src/lib.rs
@@ -0,0 +1,394 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Optional Python Code Mode interpreter. Each cell owns a disposable Monty
+//! process; only the Host can admit tools, publish output or commit JSON state.
+mod helpers;
+mod protocol;
+mod value;
+pub mod worker;
+
+use futures_util::{StreamExt, future::BoxFuture, stream::FuturesUnordered};
+use maka_agent_facts::tools::{ToolError, ToolExecutor};
+use maka_code_runtime::{
+    CellAbort, CellContext, CellDiagnostic, CellLimits, CellResult, ToolScope, complete,
+};
+use protocol::{Reply, Request, Start};
+use serde_json::Value;
+use std::{
+    collections::{BTreeMap, HashSet},
+    path::PathBuf,
+    process::Stdio,
+    sync::Arc,
+    time::Instant,
+};
+use tokio::{
+    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+    process::{Child, ChildStdin, ChildStdout},
+    runtime::Handle,
+};
+use tokio_util::sync::CancellationToken;
+
+#[derive(Clone)]
+pub struct CodeExecutor {
+    limits: CellLimits,
+    worker: PathBuf,
+}
+impl CodeExecutor {
+    pub fn new(limits: CellLimits) -> Result<Self, CellAbort> {
+        let current = std::env::current_exe().map_err(internal)?;
+        let worker = current
+            .parent()
+            .ok_or_else(|| internal("executable has no parent directory"))?
+            .join(if cfg!(windows) {
+                "maka-python-worker.exe"
+            } else {
+                "maka-python-worker"
+            });
+        Self::with_worker(limits, worker)
+    }
+    /// Embeddings supply their packaged worker explicitly. The model never
+    /// chooses a command, environment or interpreter executable.
+    pub fn with_worker(limits: CellLimits, worker: PathBuf) -> Result<Self, CellAbort> {
+        if limits.timeout.is_zero()
+            || limits.timeout.as_millis() > u64::MAX as u128
+            || Instant::now().checked_add(limits.timeout).is_none()
+            || limits.heap_bytes < 8 * 1024 * 1024
+        {
+            return Err(internal("invalid cell limits"));
+        }
+        Ok(Self { limits, worker })
+    }
+    pub fn limits(&self) -> &CellLimits {
+        &self.limits
+    }
+    pub async fn execute_module(
+        &self,
+        source: String,
+        tools: Arc<dyn ToolExecutor>,
+        cancellation: CancellationToken,
+        context: CellContext,
+    ) -> Result<CellResult, CellAbort> {
+        if cancellation.is_cancelled() {
+            return Err(CellAbort::Cancelled);
+        }
+        let cancellation = cancellation.child_token();
+        let _cancel_on_drop = cancellation.clone().drop_guard();
+        let executor = self.clone();
+        // The owner task survives a dropped observer until child reaping and
+        // every admitted Host effect have settled.
+        tokio::spawn(async move { executor.run(source, tools, cancellation, context).await })
+            .await
+            .map_err(internal)?
+    }
+    async fn run(
+        &self,
+        source: String,
+        tools: Arc<dyn ToolExecutor>,
+        cancellation: CancellationToken,
+        context: CellContext,
+    ) -> Result<CellResult, CellAbort> {
+        let names = tools.names();
+        let mut unique = HashSet::new();
+        if names
+            .iter()
+            .any(|name| !unique.insert(tool_identifier(name)))
+        {
+            return Ok(complete(
+                Err(helpers::error(
+                    "tool names collide after Python normalization",
+                )),
+                vec![],
+            ));
+        }
+        let scope = ToolScope::new(
+            context.clone(),
+            tools,
+            cancellation.child_token(),
+            Handle::current(),
+        );
+        let mut child = tokio::process::Command::new(&self.worker)
+            .args([
+                self.limits.heap_bytes.to_string(),
+                self.limits.timeout.as_millis().to_string(),
+            ])
+            .env_clear()
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::null())
+            .kill_on_drop(true)
+            .spawn()
+            .map_err(|error| {
+                internal(format!(
+                    "cannot start bundled Python worker {}: {error}",
+                    self.worker.display()
+                ))
+            })?;
+        let mut input = child
+            .stdin
+            .take()
+            .ok_or_else(|| internal("missing worker input"))?;
+        let mut output = BufReader::new(
+            child
+                .stdout
+                .take()
+                .ok_or_else(|| internal("missing worker output"))?,
+        );
+        let start = Start {
+            source,
+            names,
+            metadata: context.metadata().to_vec(),
+        };
+        let result = tokio::select! {
+            biased;
+            _=cancellation.cancelled()=>Err(CellAbort::Cancelled),
+            result=drive(&mut input,&mut output,start,&scope,&context,&self.limits)=>result,
+        };
+        scope.cancel();
+        drop(input);
+        let status = reap(&mut child).await;
+        scope.settle().await;
+        if let Some(error) = context.failure() {
+            return Err(CellAbort::Tool(error));
+        }
+        let status = status?;
+        if cancellation.is_cancelled() {
+            return Err(CellAbort::Cancelled);
+        }
+        if status.code() == Some(monty_types::OOM_EXIT_CODE) {
+            return Ok(complete(
+                Err(CellDiagnostic::limit("Python worker memory limit exceeded")),
+                scope.calls(),
+            ));
+        }
+        Ok(complete(result?, scope.calls()))
+    }
+}
+
+type Pending = FuturesUnordered<BoxFuture<'static, (u32, Result<Value, CellDiagnostic>)>>;
+async fn drive(
+    input: &mut ChildStdin,
+    output: &mut BufReader<ChildStdout>,
+    start: Start,
+    scope: &Arc<ToolScope>,
+    context: &CellContext,
+    limits: &CellLimits,
+) -> Result<Result<Value, CellDiagnostic>, CellAbort> {
+    send(input, &start).await?;
+    let mut remaining = limits.timeout;
+    let mut pending = Pending::new();
+    let mut ready = BTreeMap::new();
+    let mut admitted = HashSet::new();
+    loop {
+        let started = Instant::now();
+        let request = tokio::time::timeout(
+            remaining,
+            receive(output, limits.heap_bytes.saturating_add(4 * 1024 * 1024)),
+        )
+        .await;
+        remaining = remaining.saturating_sub(started.elapsed());
+        let request = match request {
+            Ok(request) => request?,
+            Err(_) => {
+                return Ok(Err(CellDiagnostic::limit(
+                    "Python execution budget exceeded",
+                )));
+            }
+        };
+        let reply = match request {
+            Request::Complete { error } => {
+                return Ok(match error {
+                    Some(error) => Err(error),
+                    None => Ok(Value::Null),
+                });
+            }
+            Request::Call {
+                id,
+                name,
+                args,
+                kwargs,
+            } => {
+                if let Some(name) = name.strip_prefix("tool:") {
+                    let input = match (args.as_slice(), kwargs.is_empty()) {
+                        ([value], true) => Ok(value.clone()),
+                        ([], false) => Ok(Value::Object(kwargs)),
+                        ([], true) => Ok(serde_json::json!({})),
+                        _ => Err(helpers::error(
+                            "tool expects one JSON argument or keyword arguments",
+                        )),
+                    };
+                    match input.and_then(|input| scope.start(name.into(), input)) {
+                        Ok(receiver) => {
+                            if !admitted.insert(id) {
+                                return Err(internal("duplicate Python call identity"));
+                            }
+                            pending.push(Box::pin(async move {
+                                (
+                                    id,
+                                    receiver.await.unwrap_or_else(|_| {
+                                        Err(helpers::error("tool outcome unavailable"))
+                                    }),
+                                )
+                            }));
+                            Reply::Pending
+                        }
+                        Err(error) => Reply::Error { error },
+                    }
+                } else if let Some(name) = name.strip_prefix("helper:") {
+                    match helpers::invoke(name, args, kwargs, context) {
+                        Ok(_) if name == "exit" => return Ok(Ok(Value::Null)),
+                        Ok(_) if name == "yield_control" => {
+                            admitted.insert(id);
+                            pending.push(Box::pin(async move {
+                                tokio::task::yield_now().await;
+                                (id, Ok(Value::Null))
+                            }));
+                            Reply::Pending
+                        }
+                        Ok(value) => Reply::Return { value },
+                        Err(error) => Reply::Error { error },
+                    }
+                } else {
+                    Reply::Error {
+                        error: helpers::error("unknown external function"),
+                    }
+                }
+            }
+            Request::Sleep {
+                id,
+                duration,
+                asynchronous,
+            } => {
+                if Instant::now().checked_add(duration).is_none() {
+                    Reply::Error {
+                        error: helpers::error("sleep duration is too large"),
+                    }
+                } else if asynchronous {
+                    admitted.insert(id);
+                    pending.push(Box::pin(async move {
+                        tokio::time::sleep(duration).await;
+                        (id, Ok(Value::Null))
+                    }));
+                    Reply::Pending
+                } else {
+                    tokio::time::sleep(duration).await;
+                    Reply::Return { value: Value::Null }
+                }
+            }
+            Request::Resolve { ids } => {
+                if ids.is_empty() || ids.iter().any(|id| !admitted.contains(id)) {
+                    return Err(internal("unknown pending Python call"));
+                }
+                while !ids.iter().any(|id| ready.contains_key(id)) {
+                    let (id, result) = pending
+                        .next()
+                        .await
+                        .ok_or_else(|| internal("Python futures lost their owner"))?;
+                    ready.insert(id, result);
+                }
+                let results = ids
+                    .into_iter()
+                    .filter_map(|id| {
+                        ready.remove(&id).map(|result| {
+                            admitted.remove(&id);
+                            (id, result)
+                        })
+                    })
+                    .collect();
+                Reply::Resolved { results }
+            }
+        };
+        send(input, &reply).await?;
+    }
+}
+async fn send(input: &mut ChildStdin, message: &impl serde::Serialize) -> Result<(), CellAbort> {
+    let mut bytes = serde_json::to_vec(message).map_err(internal)?;
+    bytes.push(b'\n');
+    input.write_all(&bytes).await.map_err(internal)?;
+    input.flush().await.map_err(internal)
+}
+async fn receive(
+    output: &mut BufReader<ChildStdout>,
+    max_bytes: usize,
+) -> Result<Request, CellAbort> {
+    let mut line = Vec::new();
+    loop {
+        let bytes = output.fill_buf().await.map_err(internal)?;
+        if bytes.is_empty() {
+            return Err(internal("Python worker ended without a terminal frame"));
+        }
+        let end = bytes
+            .iter()
+            .position(|byte| *byte == b'\n')
+            .map(|index| index + 1);
+        let count = end.unwrap_or(bytes.len());
+        if line.len().saturating_add(count) > max_bytes {
+            return Err(internal("Python worker frame exceeds its memory budget"));
+        }
+        line.extend_from_slice(&bytes[..count]);
+        output.consume(count);
+        if end.is_some() {
+            return serde_json::from_slice(&line).map_err(internal);
+        }
+    }
+}
+async fn reap(child: &mut Child) -> Result<std::process::ExitStatus, CellAbort> {
+    let cleanup = |error| {
+        CellAbort::Tool(ToolError::CleanupUnconfirmed(format!(
+            "Python worker cleanup: {error}"
+        )))
+    };
+    if let Some(status) = child.try_wait().map_err(cleanup)? {
+        return Ok(status);
+    }
+    child.start_kill().map_err(cleanup)?;
+    child.wait().await.map_err(cleanup)
+}
+fn internal(error: impl std::fmt::Display) -> CellAbort {
+    CellAbort::Internal(error.to_string())
+}
+
+pub fn tool_identifier(name: &str) -> String {
+    let mut result: String = name
+        .chars()
+        .enumerate()
+        .map(|(index, ch)| {
+            if ch == '_' || ch.is_ascii_alphabetic() || (index > 0 && ch.is_ascii_digit()) {
+                ch
+            } else {
+                '_'
+            }
+        })
+        .collect();
+    if result.is_empty() {
+        result.push('_');
+    }
+    // Keywords cannot be attributes in Python source, even after a dot.
+    if [
+        "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
+        "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
+        "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
+        "try", "while", "with", "yield",
+    ]
+    .contains(&result.as_str())
+    {
+        result.push('_');
+    }
+    result
+}
diff --git a/platform/python-runtime/src/main.rs b/platform/python-runtime/src/main.rs
new file mode 100644
index 0000000000..a3988911db
--- /dev/null
+++ b/platform/python-runtime/src/main.rs
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#[global_allocator]
+static ALLOCATOR: monty_alloc::LimitedAllocator = monty_alloc::LimitedAllocator;
+
+fn main() {
+    if let Err(error) = maka_python_runtime::worker::run_stdio() {
+        eprintln!("Python worker: {error}");
+        std::process::exit(1);
+    }
+}
diff --git a/platform/python-runtime/src/protocol.rs b/platform/python-runtime/src/protocol.rs
new file mode 100644
index 0000000000..35634b9bd6
--- /dev/null
+++ b/platform/python-runtime/src/protocol.rs
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_code_runtime::{CellDiagnostic, ToolMetadata};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::time::Duration;
+
+#[derive(Serialize, Deserialize)]
+pub(crate) struct Start {
+    pub source: String,
+    pub names: Vec<String>,
+    pub metadata: Vec<ToolMetadata>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub(crate) enum Request {
+    Call {
+        id: u32,
+        name: String,
+        args: Vec<Value>,
+        kwargs: serde_json::Map<String, Value>,
+    },
+    Sleep {
+        id: u32,
+        duration: Duration,
+        asynchronous: bool,
+    },
+    Resolve {
+        ids: Vec<u32>,
+    },
+    Complete {
+        error: Option<CellDiagnostic>,
+    },
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub(crate) enum Reply {
+    Return {
+        value: Value,
+    },
+    Error {
+        error: CellDiagnostic,
+    },
+    Pending,
+    Resolved {
+        results: Vec<(u32, Result<Value, CellDiagnostic>)>,
+    },
+}
diff --git a/platform/python-runtime/src/value.rs b/platform/python-runtime/src/value.rs
new file mode 100644
index 0000000000..bdae781168
--- /dev/null
+++ b/platform/python-runtime/src/value.rs
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use monty_types::{MontyObject, ObjectRef};
+use serde_json::{Number, Value};
+
+pub(crate) fn from_json(value: Value) -> MontyObject {
+    match value {
+        Value::Null => MontyObject::none(),
+        Value::Bool(value) => MontyObject::bool(value),
+        Value::String(value) => MontyObject::string(value),
+        Value::Number(value) => {
+            if let Some(value) = value.as_i64() {
+                MontyObject::int(value)
+            } else if let Some(value) = value.as_u64() {
+                MontyObject::bigint(value.into())
+            } else {
+                MontyObject::float(value.as_f64().expect("JSON number"))
+            }
+        }
+        Value::Array(values) => MontyObject::list(values.into_iter().map(from_json)),
+        Value::Object(values) => MontyObject::dict(
+            values
+                .into_iter()
+                .map(|(key, value)| (MontyObject::string(key), from_json(value))),
+        ),
+    }
+}
+
+pub(crate) fn to_json(value: ObjectRef<'_>) -> Result<Value, String> {
+    convert(value, 0)
+}
+
+fn convert(value: ObjectRef<'_>, depth: usize) -> Result<Value, String> {
+    // The JSON transport uses serde_json's default recursion boundary (128).
+    // Cycles also terminate here; they can never silently become repr strings.
+    if depth >= 128 {
+        return Err("JSON value is cyclic or exceeds the transport nesting limit".into());
+    }
+    Ok(match value.type_name() {
+        "NoneType" => Value::Null,
+        "bool" => Value::Bool(value.as_bool().unwrap()),
+        "str" => Value::String(value.as_str().unwrap().into()),
+        "int" => {
+            let number = value
+                .py_repr()
+                .parse::<Number>()
+                .map_err(|_| "integer is outside the JSON number range")?;
+            if !number.is_i64() && !number.is_u64() {
+                return Err("integer is outside the JSON integer range".into());
+            }
+            Value::Number(number)
+        }
+        "float" => Value::Number(
+            Number::from_f64(value.as_float().unwrap()).ok_or("non-finite number is not JSON")?,
+        ),
+        "list" | "tuple" => Value::Array(
+            value
+                .items()
+                .unwrap()
+                .into_iter()
+                .map(|item| convert(item, depth + 1))
+                .collect::<Result<_, _>>()?,
+        ),
+        "dict" => {
+            let mut values = serde_json::Map::new();
+            for (key, item) in value.pairs().unwrap() {
+                let key = key.as_str().ok_or("JSON object keys must be strings")?;
+                values.insert(key.into(), convert(item, depth + 1)?);
+            }
+            Value::Object(values)
+        }
+        name => return Err(format!("{name} cannot cross the JSON boundary")),
+    })
+}
diff --git a/platform/python-runtime/src/worker.rs b/platform/python-runtime/src/worker.rs
new file mode 100644
index 0000000000..2c4ff4a156
--- /dev/null
+++ b/platform/python-runtime/src/worker.rs
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::{
+    protocol::{Reply, Request, Start},
+    value::{from_json, to_json},
+};
+use maka_code_runtime::{CellDiagnostic, CellDiagnosticKind};
+use monty::{MontyRun, RunProgress};
+use monty_types::{
+    CompileOptions, ExcType, ExtFunctionResult, MontyException, MontyObject, MontyUuid,
+    NameLookupResult, OsFunctionCall, OsPolicy, PrintWriter, ResourceLimits, ResourceTracker,
+    SleepMode,
+};
+use std::{
+    io::{self, BufRead, Write},
+    time::Duration,
+};
+
+const HELPERS: &[&str] = &[
+    "text",
+    "image",
+    "audio",
+    "generated_image",
+    "notify",
+    "store",
+    "load",
+    "yield_control",
+    "exit",
+];
+
+/// Internal entry point of the bundled worker executable. Its allocator must be
+/// monty_alloc::LimitedAllocator; never install that allocator in the Host.
+pub fn run_stdio() -> Result<(), Box<dyn std::error::Error>> {
+    let mut args = std::env::args().skip(1);
+    let heap: usize = args.next().ok_or("missing heap budget")?.parse()?;
+    let timeout: u64 = args.next().ok_or("missing execution budget")?.parse()?;
+    monty_alloc::set_hard_limit(monty_types::memory_limit_with_headroom(Some(heap), false))?;
+    let stdin = io::stdin();
+    let stdout = io::stdout();
+    let mut input = stdin.lock();
+    let mut output = stdout.lock();
+    let start: Start = read(&mut input)?;
+    let result = execute(
+        start,
+        ResourceLimits {
+            max_feed_duration: Some(Duration::from_millis(timeout)),
+            max_memory: Some(heap),
+            ..Default::default()
+        },
+        &mut input,
+        &mut output,
+    );
+    let error = match result {
+        Ok(()) => None,
+        Err(WorkerError::Python(error)) => Some(diagnostic(error)),
+        Err(WorkerError::Diagnostic(error)) => Some(error),
+        Err(WorkerError::Protocol(error)) => return Err(error),
+    };
+    write(&mut output, &Request::Complete { error })?;
+    Ok(())
+}
+
+enum WorkerError {
+    Diagnostic(CellDiagnostic),
+    Python(MontyException),
+    Protocol(Box<dyn std::error::Error>),
+}
+impl From<MontyException> for WorkerError {
+    fn from(error: MontyException) -> Self {
+        Self::Python(error)
+    }
+}
+impl From<Box<dyn std::error::Error>> for WorkerError {
+    fn from(error: Box<dyn std::error::Error>) -> Self {
+        Self::Protocol(error)
+    }
+}
+
+fn execute(
+    start: Start,
+    limits: ResourceLimits,
+    input: &mut impl BufRead,
+    output: &mut impl Write,
+) -> Result<(), WorkerError> {
+    let mut inputs: Vec<_> = HELPERS
+        .iter()
+        .map(|name| MontyObject::function(format!("helper:{name}"), None))
+        .collect();
+    let mut names: Vec<String> = HELPERS.iter().map(|name| (*name).into()).collect();
+    names.extend(["tools".into(), "ALL_TOOLS".into()]);
+    inputs.push(MontyObject::module(
+        "tools",
+        MontyUuid::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
+        start.names.into_iter().map(|name| {
+            (
+                MontyObject::string(crate::tool_identifier(&name)),
+                MontyObject::function(format!("tool:{name}"), None),
+            )
+        }),
+    ));
+    inputs.push(MontyObject::tuple(start.metadata.into_iter().map(|tool| {
+        MontyObject::named_tuple(
+            "Tool",
+            ["name", "description"],
+            [
+                MontyObject::string(tool.name),
+                MontyObject::string(tool.description),
+            ],
+        )
+    })));
+    let runner = match MontyRun::new(
+        start.source,
+        "code_mode.py",
+        names,
+        CompileOptions::default(),
+    ) {
+        Ok(runner) => runner.with_os_policy(OsPolicy {
+            sleep: SleepMode::CallHost,
+            ..Default::default()
+        }),
+        Err(error) => {
+            return Err(WorkerError::Diagnostic(CellDiagnostic::new(
+                CellDiagnosticKind::ParseError,
+                error.to_string(),
+            )));
+        }
+    };
+    let mut progress = runner.start(inputs, ResourceTracker::new(limits), PrintWriter::Disabled)?;
+    loop {
+        progress = match progress {
+            RunProgress::Complete(_) => return Ok(()),
+            RunProgress::FunctionCall(call) => {
+                let arguments = (|| -> Result<_, String> {
+                    let args = call.args.args().map(to_json).collect::<Result<_, _>>()?;
+                    let kwargs = call
+                        .args
+                        .kwargs()
+                        .map(|(key, value)| {
+                            Ok((
+                                key.as_str().ok_or("keyword must be a string")?.into(),
+                                to_json(value)?,
+                            ))
+                        })
+                        .collect::<Result<_, String>>()?;
+                    Ok((args, kwargs))
+                })();
+                let reply = match arguments {
+                    Ok((args, kwargs)) => exchange(
+                        input,
+                        output,
+                        &Request::Call {
+                            id: call.call_id,
+                            name: call.function_name.clone(),
+                            args,
+                            kwargs,
+                        },
+                    )?,
+                    Err(error) => Reply::Error {
+                        error: CellDiagnostic::new(CellDiagnosticKind::ExecutionError, error),
+                    },
+                };
+                match reply {
+                    Reply::Pending => call.resume_pending(PrintWriter::Disabled)?,
+                    reply => call.resume(reply_result(reply)?, PrintWriter::Disabled)?,
+                }
+            }
+            RunProgress::ResolveFutures(wait) => {
+                let reply = exchange(
+                    input,
+                    output,
+                    &Request::Resolve {
+                        ids: wait.pending_call_ids().to_vec(),
+                    },
+                )?;
+                let Reply::Resolved { results } = reply else {
+                    return Err(protocol("expected resolved futures"));
+                };
+                wait.resume(
+                    results
+                        .into_iter()
+                        .map(|(id, result)| (id, external(result)))
+                        .collect(),
+                    PrintWriter::Disabled,
+                )?
+            }
+            RunProgress::OsCall(call) => {
+                let (duration, asynchronous) = match call.function_call {
+                    OsFunctionCall::Sleep(duration) => (duration, false),
+                    OsFunctionCall::AsyncSleep(duration) => (duration, true),
+                    _ => {
+                        progress = call.resume(
+                            MontyException::runtime_error(
+                                "direct OS access is unavailable; use an authorized Host tool",
+                            ),
+                            PrintWriter::Disabled,
+                        )?;
+                        continue;
+                    }
+                };
+                let reply = exchange(
+                    input,
+                    output,
+                    &Request::Sleep {
+                        id: call.call_id,
+                        duration,
+                        asynchronous,
+                    },
+                )?;
+                let result = match reply {
+                    Reply::Pending => ExtFunctionResult::Future(call.call_id),
+                    reply => reply_result(reply)?,
+                };
+                call.resume(result, PrintWriter::Disabled)?
+            }
+            RunProgress::NameLookup(lookup) => {
+                lookup.resume(NameLookupResult::Undefined, PrintWriter::Disabled)?
+            }
+        };
+    }
+}
+
+fn external(result: Result<serde_json::Value, CellDiagnostic>) -> ExtFunctionResult {
+    match result {
+        Ok(value) => ExtFunctionResult::Return(from_json(value)),
+        Err(error) => ExtFunctionResult::Error(MontyException::runtime_error(error.message)),
+    }
+}
+fn reply_result(reply: Reply) -> Result<ExtFunctionResult, WorkerError> {
+    match reply {
+        Reply::Return { value } => Ok(external(Ok(value))),
+        Reply::Error { error } => Ok(external(Err(error))),
+        _ => Err(protocol("expected function result")),
+    }
+}
+fn protocol(message: &str) -> WorkerError {
+    WorkerError::Protocol(message.into())
+}
+fn exchange(
+    input: &mut impl BufRead,
+    output: &mut impl Write,
+    request: &Request,
+) -> Result<Reply, Box<dyn std::error::Error>> {
+    write(output, request)?;
+    read(input)
+}
+fn read<T: serde::de::DeserializeOwned>(
+    input: &mut impl BufRead,
+) -> Result<T, Box<dyn std::error::Error>> {
+    let mut line = String::new();
+    if input.read_line(&mut line)? == 0 {
+        return Err("Host closed the worker protocol".into());
+    }
+    Ok(serde_json::from_str(&line)?)
+}
+fn write(
+    output: &mut impl Write,
+    message: &impl serde::Serialize,
+) -> Result<(), Box<dyn std::error::Error>> {
+    serde_json::to_writer(&mut *output, message)?;
+    output.write_all(b"\n")?;
+    output.flush()?;
+    Ok(())
+}
+fn diagnostic(error: MontyException) -> CellDiagnostic {
+    let kind = match error.exc_type() {
+        ExcType::MemoryError | ExcType::TimeoutError => CellDiagnosticKind::LimitExceeded,
+        _ => CellDiagnosticKind::ExecutionError,
+    };
+    CellDiagnostic::new(kind, error.to_string())
+}
diff --git a/platform/python-runtime/tests/runtime.rs b/platform/python-runtime/tests/runtime.rs
new file mode 100644
index 0000000000..f91c3614c1
--- /dev/null
+++ b/platform/python-runtime/tests/runtime.rs
@@ -0,0 +1,413 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_agent_facts::tools::{ToolError, ToolExecutor, ToolFuture};
+use maka_code_runtime::{
+    CellAbort, CellContext, CellDiagnosticKind, CellLimits, CellOutput, CellResult, CellStore,
+    ToolMetadata,
+};
+use maka_python_runtime::CodeExecutor;
+use serde_json::{Value, json};
+use std::{
+    sync::{
+        Arc,
+        atomic::{AtomicBool, Ordering},
+    },
+    time::Duration,
+};
+use tokio::sync::{Barrier, Notify};
+use tokio_util::sync::CancellationToken;
+
+struct Tools<F>(F);
+impl<F> ToolExecutor for Tools<F>
+where
+    F: Fn(String, Value, CancellationToken) -> ToolFuture + Send + Sync + 'static,
+{
+    fn names(&self) -> Vec<String> {
+        vec!["echo".into(), "wait".into()]
+    }
+    fn invoke(&self, name: String, input: Value, cancellation: CancellationToken) -> ToolFuture {
+        (self.0)(name, input, cancellation)
+    }
+}
+fn echo() -> Arc<dyn ToolExecutor> {
+    Arc::new(Tools(|_, input, _| -> ToolFuture {
+        Box::pin(async move { Ok(input) })
+    }))
+}
+fn executor(limits: CellLimits) -> CodeExecutor {
+    CodeExecutor::with_worker(limits, env!("CARGO_BIN_EXE_maka-python-worker").into()).unwrap()
+}
+fn context() -> CellContext {
+    CellContext::new(
+        CellStore::default(),
+        vec![ToolMetadata {
+            name: "echo".into(),
+            description: "echo JSON".into(),
+        }],
+    )
+}
+fn success(result: CellResult) -> usize {
+    match result {
+        CellResult::Success { value, tool_calls } => {
+            assert_eq!(value, Value::Null);
+            tool_calls.len()
+        }
+        other => panic!("{other:?}"),
+    }
+}
+async fn run(source: &str, context: CellContext) -> Result<CellResult, CellAbort> {
+    executor(CellLimits::default())
+        .execute_module(source.into(), echo(), CancellationToken::new(), context)
+        .await
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn python_fanout_and_partial_failure_use_real_host_tools() {
+    let barrier = Arc::new(Barrier::new(2));
+    let tools = Arc::new(Tools(move |_, input: Value, _| -> ToolFuture {
+        let barrier = barrier.clone();
+        Box::pin(async move {
+            barrier.wait().await;
+            if input["id"] == 2 {
+                Err(ToolError::Failed("failed-two".into()))
+            } else {
+                Ok(input)
+            }
+        })
+    }));
+    let context = context();
+    let result = tokio::time::timeout(
+        Duration::from_secs(10),
+        executor(CellLimits::default()).execute_module(
+            r#"
+import asyncio
+async def settled(n):
+    try:
+        return {"ok": True, "value": await tools.echo(id=n)}
+    except Exception as exc:
+        return {"ok": False, "error": str(exc)}
+text(await asyncio.gather(settled(1), settled(2)))
+"#
+            .into(),
+            tools,
+            CancellationToken::new(),
+            context.clone(),
+        ),
+    )
+    .await
+    .unwrap()
+    .unwrap();
+    assert_eq!(success(result), 2);
+    let outputs = context.take_output();
+    let CellOutput::Text { text } = &outputs[0] else {
+        panic!("{outputs:?}")
+    };
+    let output: Value = serde_json::from_str(text).unwrap();
+    assert_eq!(output[0], json!({"ok":true,"value":{"id":1}}));
+    assert_eq!(output[1]["ok"], false);
+    assert!(output[1]["error"].as_str().unwrap().contains("failed-two"));
+}
+
+#[tokio::test]
+async fn output_store_metadata_and_fresh_globals_obey_cell_contract() {
+    let store = CellStore::default();
+    let first = CellContext::new(
+        store.clone(),
+        vec![ToolMetadata {
+            name: "echo".into(),
+            description: "echo JSON".into(),
+        }],
+    );
+    success(run(r#"
+secret = "private"
+print(secret)
+store("receipt", {"text": "你好", "values": [1, True, None]})
+text(ALL_TOOLS[0].name)
+image({"type":"image", "data":"R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", "mimeType":"image/gif"}, detail="original")
+notify("ready")
+await yield_control()
+secret
+"#,first.clone()).await.unwrap());
+    let outputs = first.take_output();
+    assert_eq!(outputs.len(), 2);
+    assert!(matches!(&outputs[0],CellOutput::Text{text} if text=="echo"));
+    assert!(matches!(
+        &outputs[1],
+        CellOutput::Media {
+            detail: Some(maka_agent_facts::tool_output::ImageDetail::Original),
+            ..
+        }
+    ));
+    assert_eq!(
+        first.take_notifications().into_iter().collect::<Vec<_>>(),
+        ["ready"]
+    );
+    first.commit().unwrap();
+    let second = CellContext::new(store, vec![]);
+    success(
+        run(
+            "text(load('receipt'))\ntry:\n    text(secret)\nexcept NameError:\n    text('fresh')",
+            second.clone(),
+        )
+        .await
+        .unwrap(),
+    );
+    let outputs = second.take_output();
+    assert!(matches!(&outputs[1],CellOutput::Text{text} if text=="fresh"));
+    let CellOutput::Text { text } = &outputs[0] else {
+        panic!()
+    };
+    assert_eq!(
+        serde_json::from_str::<Value>(text).unwrap(),
+        json!({"text":"你好","values":[1,true,null]})
+    );
+}
+
+#[tokio::test]
+async fn json_boundary_rejects_lossy_types_and_cycles_before_effects() {
+    for value in ["{1: 'bad'}", "float('nan')", "b'bytes'", "2**100", "{1,2}"] {
+        let context = context();
+        success(
+            run(
+                &format!(
+                    "try:\n    await tools.echo({value})\nexcept Exception:\n    text('rejected')"
+                ),
+                context.clone(),
+            )
+            .await
+            .unwrap(),
+        );
+        assert!(matches!(&context.take_output()[0],CellOutput::Text{text} if text=="rejected"));
+    }
+    let result = run("a=[]\na.append(a)\nawait tools.echo(a)", context())
+        .await
+        .unwrap();
+    assert!(matches!(result,CellResult::Failure{tool_calls,..} if tool_calls.is_empty()));
+    let context = context();
+    assert_eq!(
+        success(
+            run(
+                "text(await tools.echo({'n':18446744073709551615}))",
+                context.clone()
+            )
+            .await
+            .unwrap()
+        ),
+        1
+    );
+    assert!(
+        matches!(&context.take_output()[0],CellOutput::Text{text} if text.contains("18446744073709551615"))
+    );
+}
+
+#[tokio::test]
+async fn cpu_memory_parse_and_os_errors_stay_inside_worker() {
+    for (source, limits, kind) in [
+        (
+            "while True: pass",
+            CellLimits {
+                timeout: Duration::from_millis(150),
+                ..Default::default()
+            },
+            CellDiagnosticKind::LimitExceeded,
+        ),
+        (
+            "items=['x'*32000 for i in range(10000)]",
+            CellLimits {
+                heap_bytes: 8 * 1024 * 1024,
+                ..Default::default()
+            },
+            CellDiagnosticKind::LimitExceeded,
+        ),
+        ("def", CellLimits::default(), CellDiagnosticKind::ParseError),
+        (
+            "open('/etc/passwd').read()",
+            CellLimits::default(),
+            CellDiagnosticKind::ExecutionError,
+        ),
+    ] {
+        let result = executor(limits)
+            .execute_module(source.into(), echo(), CancellationToken::new(), context())
+            .await
+            .unwrap();
+        assert!(
+            matches!(result,CellResult::Failure{error,..} if error.kind==kind),
+            "{source}"
+        );
+    }
+    success(run("text('Host still works')", context()).await.unwrap());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn tool_waits_and_python_sleep_do_not_spend_execution_budget() {
+    let tools = Arc::new(Tools(|_, input, _| -> ToolFuture {
+        Box::pin(async move {
+            tokio::time::sleep(Duration::from_millis(400)).await;
+            Ok(input)
+        })
+    }));
+    let result = executor(CellLimits {
+        timeout: Duration::from_millis(200),
+        ..Default::default()
+    })
+    .execute_module(
+        "import asyncio\nawait tools.wait({})\nawait asyncio.sleep(0.4)\ntext('done')".into(),
+        tools,
+        CancellationToken::new(),
+        context(),
+    )
+    .await
+    .unwrap();
+    assert_eq!(success(result), 1);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn completion_and_failure_cancel_unawaited_tools_and_wait_for_cleanup() {
+    for tail in [
+        "text('done')",
+        "raise ValueError('bad')",
+        "exit()\nawait tools.echo({})",
+    ] {
+        let entered = Arc::new(Notify::new());
+        let cancelled = Arc::new(Notify::new());
+        let release = Arc::new(Notify::new());
+        let tools = {
+            let entered = entered.clone();
+            let cancelled = cancelled.clone();
+            let release = release.clone();
+            Arc::new(Tools(
+                move |name: String, _, cancel: CancellationToken| -> ToolFuture {
+                    let entered = entered.clone();
+                    let cancelled = cancelled.clone();
+                    let release = release.clone();
+                    Box::pin(async move {
+                        if name == "echo" {
+                            entered.notified().await;
+                            return Ok(Value::Null);
+                        }
+                        entered.notify_one();
+                        cancel.cancelled().await;
+                        cancelled.notify_one();
+                        release.notified().await;
+                        Ok(Value::Null)
+                    })
+                },
+            ))
+        };
+        let source = format!("tools.wait({{}})\nawait tools.echo({{}})\n{tail}");
+        let mut task = tokio::spawn(async move {
+            executor(CellLimits::default())
+                .execute_module(source, tools, CancellationToken::new(), context())
+                .await
+        });
+        tokio::time::timeout(Duration::from_secs(5), cancelled.notified())
+            .await
+            .unwrap();
+        assert!(
+            tokio::time::timeout(Duration::from_millis(30), &mut task)
+                .await
+                .is_err()
+        );
+        release.notify_one();
+        let result = task.await.unwrap().unwrap();
+        match result {
+            CellResult::Success { tool_calls, .. } | CellResult::Failure { tool_calls, .. } => {
+                assert_eq!(tool_calls.len(), 2)
+            }
+        }
+    }
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn cancellation_and_dropped_observer_drain_the_owned_effect() {
+    for drop_observer in [false, true] {
+        let started = Arc::new(Notify::new());
+        let cleaned = Arc::new(AtomicBool::new(false));
+        let tools = {
+            let started = started.clone();
+            let cleaned = cleaned.clone();
+            Arc::new(Tools(
+                move |_, _, cancel: CancellationToken| -> ToolFuture {
+                    let started = started.clone();
+                    let cleaned = cleaned.clone();
+                    Box::pin(async move {
+                        started.notify_one();
+                        cancel.cancelled().await;
+                        tokio::time::sleep(Duration::from_millis(30)).await;
+                        cleaned.store(true, Ordering::SeqCst);
+                        Ok(Value::Null)
+                    })
+                },
+            ))
+        };
+        let cancellation = CancellationToken::new();
+        let token = cancellation.clone();
+        let task = tokio::spawn(async move {
+            executor(CellLimits::default())
+                .execute_module("await tools.wait({})".into(), tools, token, context())
+                .await
+        });
+        tokio::time::timeout(Duration::from_secs(5), started.notified())
+            .await
+            .unwrap();
+        if drop_observer {
+            task.abort();
+        } else {
+            cancellation.cancel();
+            assert!(matches!(task.await.unwrap(), Err(CellAbort::Cancelled)));
+        }
+        tokio::time::timeout(Duration::from_secs(5), async {
+            while !cleaned.load(Ordering::SeqCst) {
+                tokio::task::yield_now().await;
+            }
+        })
+        .await
+        .unwrap();
+    }
+    let cancellation = CancellationToken::new();
+    let token = cancellation.clone();
+    let task = tokio::spawn(async move {
+        executor(CellLimits::default())
+            .execute_module("while True: pass".into(), echo(), token, context())
+            .await
+    });
+    tokio::time::sleep(Duration::from_millis(50)).await;
+    cancellation.cancel();
+    assert!(matches!(
+        tokio::time::timeout(Duration::from_secs(2), task)
+            .await
+            .unwrap()
+            .unwrap(),
+        Err(CellAbort::Cancelled)
+    ));
+}
+
+#[tokio::test]
+async fn caught_fatal_tool_error_cannot_publish_success() {
+    let tools = Arc::new(Tools(|_, _, _| -> ToolFuture {
+        Box::pin(async { Err(ToolError::Persistence("journal unavailable".into())) })
+    }));
+    let context = context();
+    let result=executor(CellLimits::default()).execute_module("try:\n    await tools.echo({})\nexcept Exception:\n    text('caught')\nawait tools.echo({})".into(),tools,CancellationToken::new(),context).await;
+    assert!(matches!(
+        result,
+        Err(CellAbort::Tool(ToolError::Persistence(_)))
+    ));
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org