Skip to content

feat: add embedded MicroPython VM with run_python tool#128

Open
crispyberry wants to merge 1 commit intomainfrom
feat/micropython-vm
Open

feat: add embedded MicroPython VM with run_python tool#128
crispyberry wants to merge 1 commit intomainfrom
feat/micropython-vm

Conversation

@crispyberry
Copy link
Copy Markdown
Contributor

@crispyberry crispyberry commented Mar 7, 2026

Embed MicroPython via the official embed port to give the agent the ability to execute sandboxed Python scripts (calculations, data processing, text manipulation, etc.) through a new run_python tool.

  • Add micropython_embed ESP-IDF component (mpconfigport.h disables network/filesystem/hardware modules for sandbox safety)
  • Add micropython_vm wrapper: PSRAM heap alloc, mutex, esp_timer timeout with VM hook, stdout capture via mp_hal_stdout_tx_strn
  • Register run_python tool in tool_registry and context_builder
  • Add build_micropython_embed.sh helper script
  • Update skill-creator.md with Python-enabled skills section

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Python script execution capability with automatic output capture and timeout support
    • Introduced "run_python" tool for executing Python code within skills
  • Documentation

    • Expanded skill creation guide with Python-enabled skills section and usage examples

Embed MicroPython via the official embed port to give the agent the
ability to execute sandboxed Python scripts (calculations, data
processing, text manipulation, etc.) through a new `run_python` tool.

- Add micropython_embed ESP-IDF component (mpconfigport.h disables
  network/filesystem/hardware modules for sandbox safety)
- Add micropython_vm wrapper: PSRAM heap alloc, mutex, esp_timer
  timeout with VM hook, stdout capture via mp_hal_stdout_tx_strn
- Register run_python tool in tool_registry and context_builder
- Add build_micropython_embed.sh helper script
- Update skill-creator.md with Python-enabled skills section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

This pull request integrates MicroPython VM capabilities into the application, enabling embedded Python code execution via a new tool. It adds build infrastructure, VM implementation with thread-safe execution and timeout handling, HAL stubs, tool registry integration, and configuration macros.

Changes

Cohort / File(s) Summary
Build Infrastructure
.gitignore, components/micropython_embed/CMakeLists.txt, scripts/build_micropython_embed.sh
Configures build rules for MicroPython embed component: ignore patterns for generated artifacts, conditional CMake registration based on generated file presence with warning guidance, and bash script automating source clone, mpy-cross build, port config copying, and artifact collection.
MicroPython Port Configuration
components/micropython_embed/port/mpconfigport.h, components/micropython_embed/port/mphalport.h
Defines ESP32-S3 port types, feature flags (disables filesystem/networking/threading), VM timeout hook macro for keyboard interrupt on flag, and minimal HAL stub for interrupt character handling.
MicroPython VM Core
main/micropython/micropython_vm.c, main/micropython/micropython_vm.h
Implements thread-safe VM harness with mutex, ESP timer-based timeout mechanism, SPIRAM heap allocation per execution, stdout capture to bounded buffer, and public init/exec APIs with proper resource cleanup on failure.
Tool Integration
main/tools/tool_run_python.c, main/tools/tool_run_python.h, main/tools/tool_registry.c
Adds run_python tool executor that parses JSON input, validates code and timeout fields, invokes VM execution, and returns captured output; registers tool in registry.
Initialization & Configuration
main/mimi.c, main/mimi_config.h, main/CMakeLists.txt
Initializes VM in app_main prior to subsystem startup; adds heap size, timeout, and output buffer configuration macros; links new source files (micropython_vm.c, tool_run_python.c) and depends on micropython_embed component.
Documentation
main/agent/context_builder.c, spiffs_data/skills/skill-creator.md
Documents run_python tool capabilities and restrictions in agent context; adds Python-enabled skills guide with workflow example and best practices.

Sequence Diagrams

sequenceDiagram
    participant App as Application<br/>(app_main)
    participant VM as MicroPython VM<br/>(micropython_vm.c)
    participant Timer as ESP Timer
    participant SPIRAM as SPIRAM<br/>(Heap)
    
    App->>VM: micropython_vm_init()
    VM->>VM: Initialize mutex
    VM->>Timer: Create timeout timer
    VM-->>App: Ready
    
    App->>VM: micropython_vm_exec(code,<br/>output, timeout)
    VM->>VM: Acquire mutex lock
    VM->>SPIRAM: Allocate GC heap
    VM->>Timer: Start timeout timer
    VM->>VM: Initialize interpreter
    VM->>VM: Execute Python code
    VM->>VM: Capture stdout to buffer
    VM->>VM: Teardown interpreter
    VM->>SPIRAM: Free GC heap
    VM->>Timer: Stop timeout timer
    VM->>VM: Release mutex lock
    VM-->>App: Return output/error
Loading
sequenceDiagram
    participant Tool as Tool Registry<br/>(tool_run_python)
    participant Parser as JSON Parser
    participant VM as MicroPython VM<br/>(micropython_vm_exec)
    participant Output as Output Buffer
    
    Tool->>Parser: Parse input JSON
    Parser-->>Tool: Extract code, timeout
    
    alt Validation Failed
        Tool->>Output: Write error message
        Tool-->>Tool: Return ESP_ERR_INVALID_ARG
    else Validation Passed
        Tool->>VM: Execute code with<br/>timeout constraint
        VM->>Output: Capture stdout
        VM-->>Tool: Return result
        
        alt Execution Success
            Tool-->>Tool: Return ESP_OK
        else Execution Failed
            Tool-->>Tool: Log error, return<br/>error code
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

enhancement, ready to merge

Suggested reviewers

  • IRONICBo

Poem

🐰 A whisker-twitch of joy, I say!
MicroPython now saves the day,
With timers ticking, heaps aligned,
Thread-safe sandboxes, carefully designed.
Python code shall run and thrive,
Keeping systems safe and alive! 🐍

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding an embedded MicroPython VM with a run_python tool, which aligns with the substantial additions across the codebase for MicroPython integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/micropython-vm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
scripts/build_micropython_embed.sh (3)

38-38: Quote command substitution to prevent word splitting.

ShellCheck SC2046: The $(nproc ...) output should be quoted to prevent potential word splitting issues.

Proposed fix
-make -C "${MP_DIR}/mpy-cross" -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)
+make -C "${MP_DIR}/mpy-cross" -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build_micropython_embed.sh` at line 38, The make invocation uses
unquoted command substitution for the -j argument which can cause
word-splitting; update the make command that calls make -C "${MP_DIR}/mpy-cross"
-j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2) to quote the
substitution (i.e., wrap the entire $(...) in double quotes) so the output of
the command substitution is treated as a single token; modify the line
containing that make call accordingly to prevent ShellCheck SC2046 warnings.

48-48: Quote command substitution to prevent word splitting.

Same issue as line 38 — quote the command substitution for consistency and safety.

Proposed fix
-make -C "${MP_DIR}/ports/embed" -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)
+make -C "${MP_DIR}/ports/embed" -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build_micropython_embed.sh` at line 48, The make invocation uses an
unquoted command substitution for the -j argument which can cause
word-splitting; update the make command in the embed build step (the line
invoking make -C "${MP_DIR}/ports/embed" -j$(nproc 2>/dev/null || sysctl -n
hw.ncpu 2>/dev/null || echo 2)) to quote the command substitution so the entire
CPU-count string is treated as a single argument (e.g., -j"$(nproc ... || ... ||
echo 2)"), preserving the existing redirections and fallbacks.

30-31: Consider explicit submodule existence check.

The 2>/dev/null || true pattern swallows all errors from git submodule add, making it harder to diagnose genuine failures. Consider checking if the submodule entry already exists:

Proposed alternative
-    git -C "${PROJECT_DIR}" submodule add https://github.com/micropython/micropython.git lib/micropython 2>/dev/null || true
-    git -C "${PROJECT_DIR}" submodule update --init lib/micropython
+    if ! git -C "${PROJECT_DIR}" config --file .gitmodules --get submodule.lib/micropython.path >/dev/null 2>&1; then
+        git -C "${PROJECT_DIR}" submodule add https://github.com/micropython/micropython.git lib/micropython
+    fi
+    git -C "${PROJECT_DIR}" submodule update --init lib/micropython
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build_micropython_embed.sh` around lines 30 - 31, The script
currently silences failures from `git -C "${PROJECT_DIR}" submodule add ...
lib/micropython` using `2>/dev/null || true`; instead detect whether the
submodule is already configured (e.g. check for a 'path = lib/micropython' entry
in "${PROJECT_DIR}/.gitmodules" or test existence of
"${PROJECT_DIR}/lib/micropython/.git") and only run `git submodule add` when
missing, removing the `2>/dev/null || true` suppression so legitimate errors
surface; keep the subsequent `git -C "${PROJECT_DIR}" submodule update --init
lib/micropython` as-is.
main/agent/context_builder.c (1)

49-51: Consider including heapq in the available modules list.

The system prompt lists available modules, but heapq is enabled in mpconfigport.h (line 46: MICROPY_PY_HEAPQ (1)) but not mentioned here. For consistency, consider adding it to the list or disabling it in the config if it's not intended to be available.

Proposed fix
-        "- run_python: Execute Python code via embedded MicroPython VM. Use print() for output. "
-        "Available modules: math, json, re, collections, struct, binascii, random. No network/file/hardware access.\n\n"
+        "- run_python: Execute Python code via embedded MicroPython VM. Use print() for output. "
+        "Available modules: math, json, re, collections, heapq, struct, binascii, random. No network/file/hardware access.\n\n"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/agent/context_builder.c` around lines 49 - 51, The run_python
description string in context_builder.c currently lists available MicroPython
modules but omits heapq; update that string (the literal that starts with "-
run_python: Execute Python code..." and lists "math, json, re, collections,
struct, binascii, random") to include "heapq" so the prompt matches
mpconfigport.h's MICROPY_PY_HEAPQ setting, or alternatively disable heapq in the
MicroPython config if it should not be exposed.
main/tools/tool_registry.c (1)

186-190: Make the schema match the runtime timeout contract.

The executor clamps timeout_ms to 100..30000 and defaults to MIMI_MICROPYTHON_TIMEOUT_MS, but the schema only says integer and hard-codes 10000 in the description. That metadata will drift as soon as the config changes. Add minimum / maximum, and avoid baking the default into the string unless it is generated from the macro.

📐 Suggested schema update
         .input_schema_json =
             "{\"type\":\"object\","
             "\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"Python code to execute. Use print() to produce output.\"},"
-            "\"timeout_ms\":{\"type\":\"integer\",\"description\":\"Execution timeout in milliseconds (default 10000, max 30000)\"}},"
+            "\"timeout_ms\":{\"type\":\"integer\",\"minimum\":100,\"maximum\":30000,"
+            "\"description\":\"Execution timeout in milliseconds\"}},"
             "\"required\":[\"code\"]}",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/tools/tool_registry.c` around lines 186 - 190, The input_schema_json for
the MicroPython executor (.input_schema_json in tool_registry.c) must match the
runtime contract: add "minimum":100 and "maximum":30000 to the timeout_ms
property and remove the hard-coded "10000" default from the description; either
reference the actual default macro MIMI_MICROPYTHON_TIMEOUT_MS or omit the
default text so it won't drift when the macro changes. Update the timeout_ms
property description accordingly and keep the rest of the schema intact.
main/mimi_config.h (1)

126-126: MIMI_MICROPYTHON_OUTPUT_SIZE is not wired into the execution path.

tool_run_python_execute() forwards the caller's output_size unchanged to micropython_vm_exec(), and existing callers already pass different buffer sizes. Changing this macro here will not actually change the VM's output limit, so the config knob is misleading unless the callers are updated to use it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/mimi_config.h` at line 126, The macro MIMI_MICROPYTHON_OUTPUT_SIZE is
defined but never used in the execution path; tool_run_python_execute currently
forwards the caller-provided output_size to micropython_vm_exec, so changing the
macro has no effect. Fix by wiring the macro into the call chain: update
tool_run_python_execute to default/replace the forwarded output_size with
MIMI_MICROPYTHON_OUTPUT_SIZE (or clamp the caller value to it) before calling
micropython_vm_exec, or alternatively update the existing callers to pass
MIMI_MICROPYTHON_OUTPUT_SIZE when they want the configured limit; reference
symbols: MIMI_MICROPYTHON_OUTPUT_SIZE, tool_run_python_execute,
micropython_vm_exec.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@main/CMakeLists.txt`:
- Around line 28-31: Move the build dependency for micropython_embed from the
public REQUIRES list to PRIV_REQUIRES in the main CMakeLists.txt so it is only
linked privately for the main target; update the REQUIRES/PRIV_REQUIRES entries
so micropython_embed is removed from REQUIRES and added to PRIV_REQUIRES
(micropython_vm.c remains able to use it while micropython_vm.h stays
dependency-free of MicroPython types).

In `@main/micropython/micropython_vm.c`:
- Around line 29-40: mp_hal_stdout_tx_strn currently drops data silently when
the buffer is full (using s_stdout_buf, s_stdout_size, s_stdout_pos); fix it by
adding a truncation flag (e.g. s_stdout_truncated) that is set whenever copy <
len and by attempting to append a visible marker like "…(truncated)" if there is
any remaining space (respecting s_stdout_size and null-terminator), ensuring
s_stdout_pos and termination are updated correctly; also add a helper to
reset/check s_stdout_truncated so callers can detect clipped output.
- Line 11: The build fails because micropython_vm.c includes micropython_embed.h
which is not produced or not on the compiler include path; update the build so
the header is generated and/or added to the compiler's include directories
before compiling micropython_vm.c. Modify the build rules that produce
micropython_embed.h (or add a dependency) so its generation runs prior to
compiling the target that builds micropython_vm.c, and add the directory
containing micropython_embed.h to the compiler include paths (e.g., via the
project's CMakeLists/Makefile) so the compiler can locate micropython_embed.h
during the build.
- Around line 92-125: The function currently ignores Python execution failures
because ret is never updated after calling mp_embed_exec_str; modify the code so
mp_embed_exec_str returns a status (e.g., esp_err_t or int) indicating success,
exception, or timeout, then assign that to ret (and/or set ret = ESP_ERR_TIMEOUT
when micropython_vm_timeout_flag is set) before stopping the timer and
returning; update callers of mp_embed_exec_str/mp_embed_exec_str signature and
ensure symbols mentioned (ret, mp_embed_exec_str, micropython_vm_timeout_flag,
esp_timer_stop) are used to propagate non-ESP_OK results upward.
- Around line 51-54: The timeout currently sets micropython_vm_timeout_flag in
timeout_callback but relies on mp_sched_keyboard_interrupt (which raises a
catchable KeyboardInterrupt) so user code can swallow it; instead either latch
the timeout until VM teardown or abort non-catchably. Implement one of: (A)
change timeout_callback to set a persistent latched flag (e.g., add
micropython_vm_timeout_latched) and ensure the scheduler loop that calls
mp_sched_keyboard_interrupt checks micropython_vm_timeout_latched and, once set,
forces teardown/stop without clearing the latch; or (B) have timeout_callback
trigger a non-catchable abort path by calling nlr_jump_fail()/process
termination directly instead of setting micropython_vm_timeout_flag so execution
cannot continue. Update uses of
micropython_vm_timeout_flag/mp_sched_keyboard_interrupt accordingly to respect
the chosen approach.

In `@main/tools/tool_run_python.c`:
- Around line 11-23: The function tool_run_python_execute writes into the
caller-provided output buffer before validating it; add an early guard at the
top of tool_run_python_execute to validate output and output_size (e.g., require
output != NULL and output_size > 0 per the micropython_vm_exec contract) and
return ESP_ERR_INVALID_ARG without touching output or parsing JSON if the guard
fails; ensure all existing error paths (including after cJSON_Parse failures and
code_obj checks) no longer assume output is non-NULL and keep cJSON_Delete(root)
where appropriate so there are no leaks.

---

Nitpick comments:
In `@main/agent/context_builder.c`:
- Around line 49-51: The run_python description string in context_builder.c
currently lists available MicroPython modules but omits heapq; update that
string (the literal that starts with "- run_python: Execute Python code..." and
lists "math, json, re, collections, struct, binascii, random") to include
"heapq" so the prompt matches mpconfigport.h's MICROPY_PY_HEAPQ setting, or
alternatively disable heapq in the MicroPython config if it should not be
exposed.

In `@main/mimi_config.h`:
- Line 126: The macro MIMI_MICROPYTHON_OUTPUT_SIZE is defined but never used in
the execution path; tool_run_python_execute currently forwards the
caller-provided output_size to micropython_vm_exec, so changing the macro has no
effect. Fix by wiring the macro into the call chain: update
tool_run_python_execute to default/replace the forwarded output_size with
MIMI_MICROPYTHON_OUTPUT_SIZE (or clamp the caller value to it) before calling
micropython_vm_exec, or alternatively update the existing callers to pass
MIMI_MICROPYTHON_OUTPUT_SIZE when they want the configured limit; reference
symbols: MIMI_MICROPYTHON_OUTPUT_SIZE, tool_run_python_execute,
micropython_vm_exec.

In `@main/tools/tool_registry.c`:
- Around line 186-190: The input_schema_json for the MicroPython executor
(.input_schema_json in tool_registry.c) must match the runtime contract: add
"minimum":100 and "maximum":30000 to the timeout_ms property and remove the
hard-coded "10000" default from the description; either reference the actual
default macro MIMI_MICROPYTHON_TIMEOUT_MS or omit the default text so it won't
drift when the macro changes. Update the timeout_ms property description
accordingly and keep the rest of the schema intact.

In `@scripts/build_micropython_embed.sh`:
- Line 38: The make invocation uses unquoted command substitution for the -j
argument which can cause word-splitting; update the make command that calls make
-C "${MP_DIR}/mpy-cross" -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null
|| echo 2) to quote the substitution (i.e., wrap the entire $(...) in double
quotes) so the output of the command substitution is treated as a single token;
modify the line containing that make call accordingly to prevent ShellCheck
SC2046 warnings.
- Line 48: The make invocation uses an unquoted command substitution for the -j
argument which can cause word-splitting; update the make command in the embed
build step (the line invoking make -C "${MP_DIR}/ports/embed" -j$(nproc
2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)) to quote the command
substitution so the entire CPU-count string is treated as a single argument
(e.g., -j"$(nproc ... || ... || echo 2)"), preserving the existing redirections
and fallbacks.
- Around line 30-31: The script currently silences failures from `git -C
"${PROJECT_DIR}" submodule add ... lib/micropython` using `2>/dev/null || true`;
instead detect whether the submodule is already configured (e.g. check for a
'path = lib/micropython' entry in "${PROJECT_DIR}/.gitmodules" or test existence
of "${PROJECT_DIR}/lib/micropython/.git") and only run `git submodule add` when
missing, removing the `2>/dev/null || true` suppression so legitimate errors
surface; keep the subsequent `git -C "${PROJECT_DIR}" submodule update --init
lib/micropython` as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44705e87-7757-46f6-b73f-e8f3c92688e5

📥 Commits

Reviewing files that changed from the base of the PR and between b6e11d8 and 9a6e95d.

⛔ Files ignored due to path filters (1)
  • components/micropython_embed/generated/.gitkeep is excluded by !**/generated/**
📒 Files selected for processing (15)
  • .gitignore
  • components/micropython_embed/CMakeLists.txt
  • components/micropython_embed/port/mpconfigport.h
  • components/micropython_embed/port/mphalport.h
  • main/CMakeLists.txt
  • main/agent/context_builder.c
  • main/micropython/micropython_vm.c
  • main/micropython/micropython_vm.h
  • main/mimi.c
  • main/mimi_config.h
  • main/tools/tool_registry.c
  • main/tools/tool_run_python.c
  • main/tools/tool_run_python.h
  • scripts/build_micropython_embed.sh
  • spiffs_data/skills/skill-creator.md

Comment on lines 28 to +31
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
esp_timer esp_websocket_client micropython_embed
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if MicroPython types are exposed in public headers
rg -l 'mp_int_t|mp_uint_t|mp_off_t|micropython' --type=c --type-add 'c:*.h' -g '!micropython*' main/

Repository: memovai/mimiclaw

Length of output: 100


🏁 Script executed:

# Find all header files in main/ and check which contain MicroPython references
find main/ -name "*.h" -type f | head -20

Repository: memovai/mimiclaw

Length of output: 581


🏁 Script executed:

# Check the full CMakeLists.txt to understand public header configuration
cat main/CMakeLists.txt

Repository: memovai/mimiclaw

Length of output: 1054


🏁 Script executed:

# Check if there are any includes of micropython headers in the public headers
rg 'include.*micropython|mp_int_t|mp_uint_t|mp_obj' main/ -t c --type-add 'c:*.h' | head -30

Repository: memovai/mimiclaw

Length of output: 469


🏁 Script executed:

cat main/micropython/micropython_vm.h

Repository: memovai/mimiclaw

Length of output: 871


🏁 Script executed:

# Also check if other headers in main/ that are likely public include micropython_vm.h
rg '#include.*micropython_vm' main/ -t c --type-add 'c:*.h'

Repository: memovai/mimiclaw

Length of output: 242


Move micropython_embed from REQUIRES to PRIV_REQUIRES.

The public header micropython_vm.h does not expose any MicroPython types—it only uses standard types like esp_err_t and size_t. Since micropython_embed is only needed for internal implementation in micropython_vm.c, it should be a private dependency to avoid unnecessarily exporting it to all components that depend on main.

Proposed fix
     REQUIRES
         nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
         esp_https_ota esp_event json spiffs console vfs app_update esp-tls
-        esp_timer esp_websocket_client micropython_embed
+        esp_timer esp_websocket_client
+    PRIV_REQUIRES
+        micropython_embed
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
esp_timer esp_websocket_client micropython_embed
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
PRIV_REQUIRES
micropython_embed
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/CMakeLists.txt` around lines 28 - 31, Move the build dependency for
micropython_embed from the public REQUIRES list to PRIV_REQUIRES in the main
CMakeLists.txt so it is only linked privately for the main target; update the
REQUIRES/PRIV_REQUIRES entries so micropython_embed is removed from REQUIRES and
added to PRIV_REQUIRES (micropython_vm.c remains able to use it while
micropython_vm.h stays dependency-free of MicroPython types).

#include "esp_log.h"
#include "esp_heap_caps.h"

#include "micropython_embed.h"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Wire micropython_embed.h into the build before merge.

CI is already failing on this include with micropython_embed.h: No such file or directory. In its current state, a clean checkout cannot compile this translation unit, so the feature still depends on a manual pre-generation step that the build is not performing.

🧰 Tools
🪛 GitHub Actions: Build

[error] 11-11: micropython_embed.h: No such file or directory

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/micropython/micropython_vm.c` at line 11, The build fails because
micropython_vm.c includes micropython_embed.h which is not produced or not on
the compiler include path; update the build so the header is generated and/or
added to the compiler's include directories before compiling micropython_vm.c.
Modify the build rules that produce micropython_embed.h (or add a dependency) so
its generation runs prior to compiling the target that builds micropython_vm.c,
and add the directory containing micropython_embed.h to the compiler include
paths (e.g., via the project's CMakeLists/Makefile) so the compiler can locate
micropython_embed.h during the build.

Comment on lines +29 to +40
void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len)
{
if (!s_stdout_buf) return;
size_t avail = (s_stdout_size > s_stdout_pos + 1)
? s_stdout_size - s_stdout_pos - 1
: 0;
size_t copy = (len < avail) ? len : avail;
if (copy > 0) {
memcpy(s_stdout_buf + s_stdout_pos, str, copy);
s_stdout_pos += copy;
s_stdout_buf[s_stdout_pos] = '\0';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently drop stdout after the buffer fills.

Once avail reaches zero, the rest of the script output is discarded and the call still succeeds. That can turn JSON, CSV, or other structured results into partial data with no signal to the caller. Track truncation and append a marker or return an error when output is clipped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/micropython/micropython_vm.c` around lines 29 - 40,
mp_hal_stdout_tx_strn currently drops data silently when the buffer is full
(using s_stdout_buf, s_stdout_size, s_stdout_pos); fix it by adding a truncation
flag (e.g. s_stdout_truncated) that is set whenever copy < len and by attempting
to append a visible marker like "…(truncated)" if there is any remaining space
(respecting s_stdout_size and null-terminator), ensuring s_stdout_pos and
termination are updated correctly; also add a helper to reset/check
s_stdout_truncated so callers can detect clipped output.

Comment on lines +51 to +54
static void timeout_callback(void *arg)
{
micropython_vm_timeout_flag = 1;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In MicroPython, does mp_sched_keyboard_interrupt()raise a normal catchableKeyboardInterrupt, and can execution continue after user code catches it?

💡 Result:

Yes.

  • mp_sched_keyboard_interrupt() doesn’t “hard abort” the VM; it arranges for a KeyboardInterrupt exception object to be raised the next time the VM checks pending work (“as soon as the VM resumes”). (sources.debian.org)
  • Internally, the keyboard-interrupt path sets mp_pending_exception to the preallocated keyboard-exception object and the VM later delivers it by calling nlr_raise(obj)—i.e. it’s raised like a normal Python exception. (blog.weghos.com)
  • Because it’s delivered as a normal exception, user code can catch KeyboardInterrupt with try/except KeyboardInterrupt: and execution can continue after the except block (the pending-exception slot is cleared before raising, so it won’t immediately re-trigger unless another interrupt is scheduled). (blog.weghos.com)

(Separately, at the Python level, MicroPython documents Ctrl-C as raising KeyboardInterrupt.) (micropython.org)

Citations:


The timeout is catchable and not enforced.

mp_sched_keyboard_interrupt() raises a standard KeyboardInterrupt that user code can catch with try/except and continue execution. The pending-exception flag clears before raising, so it won't re-trigger unless another interrupt is scheduled. This is not a hard execution limit. Either latch the timeout across multiple scheduling cycles until teardown, or transition to a non-catchable abort path (e.g., nlr_jump_fail() or process termination).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/micropython/micropython_vm.c` around lines 51 - 54, The timeout
currently sets micropython_vm_timeout_flag in timeout_callback but relies on
mp_sched_keyboard_interrupt (which raises a catchable KeyboardInterrupt) so user
code can swallow it; instead either latch the timeout until VM teardown or abort
non-catchably. Implement one of: (A) change timeout_callback to set a persistent
latched flag (e.g., add micropython_vm_timeout_latched) and ensure the scheduler
loop that calls mp_sched_keyboard_interrupt checks
micropython_vm_timeout_latched and, once set, forces teardown/stop without
clearing the latch; or (B) have timeout_callback trigger a non-catchable abort
path by calling nlr_jump_fail()/process termination directly instead of setting
micropython_vm_timeout_flag so execution cannot continue. Update uses of
micropython_vm_timeout_flag/mp_sched_keyboard_interrupt accordingly to respect
the chosen approach.

Comment on lines +92 to +125
esp_err_t ret = ESP_OK;

/* Allocate GC heap from PSRAM */
void *gc_heap = heap_caps_calloc(1, MIMI_MICROPYTHON_HEAP_SIZE, MALLOC_CAP_SPIRAM);
if (!gc_heap) {
snprintf(output, output_size, "Error: failed to allocate MicroPython heap (%dKB)",
MIMI_MICROPYTHON_HEAP_SIZE / 1024);
ret = ESP_ERR_NO_MEM;
goto unlock;
}

/* Setup stdout capture */
s_stdout_buf = output;
s_stdout_pos = 0;
s_stdout_size = output_size;
output[0] = '\0';

/* Reset timeout flag */
micropython_vm_timeout_flag = 0;

/* Init interpreter */
mp_embed_init(gc_heap, MIMI_MICROPYTHON_HEAP_SIZE);

/* Start timeout timer */
if (timeout_ms > 0) {
esp_timer_start_once(s_timeout_timer, (uint64_t)timeout_ms * 1000);
}

/* Execute — exceptions are printed to stdout via mp_hal_stdout_tx_strn */
mp_embed_exec_str(code);

/* Cancel timeout */
esp_timer_stop(s_timeout_timer);
micropython_vm_timeout_flag = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate script failures via esp_err_t.

ret never changes after mp_embed_exec_str(code), so uncaught Python exceptions and timeout-triggered failures only show up as printed text in output. Callers above this layer treat non-ESP_OK as failure, which means tracebacks/timeouts currently bubble up as successful tool results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/micropython/micropython_vm.c` around lines 92 - 125, The function
currently ignores Python execution failures because ret is never updated after
calling mp_embed_exec_str; modify the code so mp_embed_exec_str returns a status
(e.g., esp_err_t or int) indicating success, exception, or timeout, then assign
that to ret (and/or set ret = ESP_ERR_TIMEOUT when micropython_vm_timeout_flag
is set) before stopping the timer and returning; update callers of
mp_embed_exec_str/mp_embed_exec_str signature and ensure symbols mentioned (ret,
mp_embed_exec_str, micropython_vm_timeout_flag, esp_timer_stop) are used to
propagate non-ESP_OK results upward.

Comment on lines +11 to +23
esp_err_t tool_run_python_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}

cJSON *code_obj = cJSON_GetObjectItem(root, "code");
if (!code_obj || !cJSON_IsString(code_obj)) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: 'code' field is required and must be a string");
return ESP_ERR_INVALID_ARG;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In C, is it valid to call snprintf with a NULL destination pointer when the size argument is greater than zero?

💡 Result:

No. In standard C, snprintf may be called with a null destination pointer only when the size argument is 0. If the size argument is greater than 0, the destination pointer must point to a writable array; passing NULL is undefined behavior. [1][2]

So: snprintf(NULL, 0, ...) is valid (commonly used to compute the required length), but snprintf(NULL, n>0, ...) is not. [1][2]

Sources: [1] (cppreference) [2] (glibc manual)


🏁 Script executed:

cd main/tools && wc -l tool_run_python.c

Repository: memovai/mimiclaw

Length of output: 80


🏁 Script executed:

cat -n main/tools/tool_run_python.c | head -50

Repository: memovai/mimiclaw

Length of output: 1925


Validate the caller's output buffer before the first error path.

The parse/validation branches write to output before checking it. If a caller passes output == NULL with output_size > 0, this causes undefined behavior instead of returning ESP_ERR_INVALID_ARG; and output_size == 0 currently disagrees with the lower-level micropython_vm_exec() contract.

🛡️ Proposed guard
 esp_err_t tool_run_python_execute(const char *input_json, char *output, size_t output_size)
 {
+    if (!output || output_size == 0) {
+        return ESP_ERR_INVALID_ARG;
+    }
+    if (!input_json) {
+        snprintf(output, output_size, "Error: input JSON is required");
+        return ESP_ERR_INVALID_ARG;
+    }
+
     cJSON *root = cJSON_Parse(input_json);
     if (!root) {
         snprintf(output, output_size, "Error: invalid JSON input");
         return ESP_ERR_INVALID_ARG;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@main/tools/tool_run_python.c` around lines 11 - 23, The function
tool_run_python_execute writes into the caller-provided output buffer before
validating it; add an early guard at the top of tool_run_python_execute to
validate output and output_size (e.g., require output != NULL and output_size >
0 per the micropython_vm_exec contract) and return ESP_ERR_INVALID_ARG without
touching output or parsing JSON if the guard fails; ensure all existing error
paths (including after cJSON_Parse failures and code_obj checks) no longer
assume output is non-NULL and keep cJSON_Delete(root) where appropriate so there
are no leaks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant