feat: add embedded MicroPython VM with run_python tool#128
feat: add embedded MicroPython VM with run_python tool#128crispyberry wants to merge 1 commit intomainfrom
Conversation
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>
📝 WalkthroughWalkthroughThis 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
Sequence DiagramssequenceDiagram
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 || truepattern swallows all errors fromgit 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 includingheapqin the available modules list.The system prompt lists available modules, but
heapqis enabled inmpconfigport.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_msto100..30000and defaults toMIMI_MICROPYTHON_TIMEOUT_MS, but the schema only saysintegerand hard-codes10000in the description. That metadata will drift as soon as the config changes. Addminimum/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_SIZEis not wired into the execution path.
tool_run_python_execute()forwards the caller'soutput_sizeunchanged tomicropython_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
⛔ Files ignored due to path filters (1)
components/micropython_embed/generated/.gitkeepis excluded by!**/generated/**
📒 Files selected for processing (15)
.gitignorecomponents/micropython_embed/CMakeLists.txtcomponents/micropython_embed/port/mpconfigport.hcomponents/micropython_embed/port/mphalport.hmain/CMakeLists.txtmain/agent/context_builder.cmain/micropython/micropython_vm.cmain/micropython/micropython_vm.hmain/mimi.cmain/mimi_config.hmain/tools/tool_registry.cmain/tools/tool_run_python.cmain/tools/tool_run_python.hscripts/build_micropython_embed.shspiffs_data/skills/skill-creator.md
| 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 |
There was a problem hiding this comment.
🧩 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 -20Repository: memovai/mimiclaw
Length of output: 581
🏁 Script executed:
# Check the full CMakeLists.txt to understand public header configuration
cat main/CMakeLists.txtRepository: 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 -30Repository: memovai/mimiclaw
Length of output: 469
🏁 Script executed:
cat main/micropython/micropython_vm.hRepository: 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.
| 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" |
There was a problem hiding this comment.
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.
| 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'; | ||
| } |
There was a problem hiding this comment.
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.
| static void timeout_callback(void *arg) | ||
| { | ||
| micropython_vm_timeout_flag = 1; | ||
| } |
There was a problem hiding this comment.
🧩 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 aKeyboardInterruptexception 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_exceptionto the preallocated keyboard-exception object and the VM later delivers it by callingnlr_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
KeyboardInterruptwithtry/except KeyboardInterrupt:and execution can continue after theexceptblock (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:
- 1: https://sources.debian.org/src/micropython/1.25.0%2Bds-1/extmod/os_dupterm.c
- 2: https://blog.weghos.com/micropython/MicroPython/py/scheduler.c.html
- 3: https://blog.weghos.com/micropython/MicroPython/py/scheduler.c.html
- 4: https://micropython.org/resources/docs/en/latest/library/micropython.html?utm_source=openai
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.
| 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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
🧩 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.cRepository: memovai/mimiclaw
Length of output: 80
🏁 Script executed:
cat -n main/tools/tool_run_python.c | head -50Repository: 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.
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_pythontool.Summary by CodeRabbit
Release Notes
New Features
Documentation