Skip to content

feat(code-runner): run untrusted Node.js and Python in-process - #771

Merged
andersonleal merged 16 commits into
mainfrom
feat/code-runner-worker
Aug 12, 2026
Merged

feat(code-runner): run untrusted Node.js and Python in-process#771
andersonleal merged 16 commits into
mainfrom
feat/code-runner-worker

Conversation

@andersonleal

@andersonleal andersonleal commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes MOT-4399. Related: MOT-3971 (sandbox-code-runner — separate sandboxed-VM worker; this is the in-process engine).

What

A new code-runner worker that runs untrusted Node.js and Python in-process, replacing the retired node-engine and python-engine workers:

  • code-runner::run executes guest code in V8 isolates (Node) or an embedded interpreter with cross-call persistence (Python). Both languages get a real iii client.
  • register_function lets guest code register live bus functions, optionally carrying request/response JSON Schemas to the catalog (functions::info). Schemas are catalog metadata, not call-time validation — docs say so explicitly. Namespaces are permissive; only the exact seeded worker ids are protected.
  • Injectable console UI: purpose-built cards for the three ops, plus a custom configuration form (below).
  • Output caps at the source (max_result_bytes 32 KiB / max_stream_bytes 16 KiB, 0 = off): oversized results are replaced by a marker string naming the size and shape with an iii.files recovery hint; stdout/stderr keep a 60/40 head+tail around a truncation marker. Motivated by a live session where a 100k-element array echoed a 589 KB result (~40k tokens) into context. The cap seam is Manager::run's single Ok-return, covering all translate builders and both engines; transport errors carry no payload.
  • Config served by the configuration worker (Tier 1 per docs/sops/configuration.md): no committed config.yaml; --config is a one-time seed. The output caps and timeouts are read per call from an ArcSwap snapshot the configuration:updated trigger swaps, so they hot-apply on save; the engine-structural fields (runtime count, V8 memory, scratch) are boot-captured and apply at the next restart — the reload handler logs that split. code-runner::on-config-change is seeded into the runtime-id registry and denied in iii-permissions.yaml.
  • Custom config form on the console's Workers tab via host.configForms.register('code-runner', …): sections labeled hot-apply vs restart-applied, byte fields with KiB/token hints, worst-case scratch footprint computed inline.

Notes

  • Guest-visible strings in node-core no longer name the retired node-engine worker (script origins, shutdown hints, error codes, default namespace mints).
  • Tests: hermetic V8 router tests in manager.rs (cap seam mutation-tested, plus a hot-swap test proving a config swap changes the very next response); tests/e2e exists but is CI-unwired.
  • Registered in .github/release-workers.yaml; CI worker discovery verified locally.

Summary by CodeRabbit

  • New Features

    • Added a code runner for executing Node.js and Python code in isolated environments.
    • Supports one-shot and persistent runtimes, function registration, teardown, timeouts, output limits, and guest callbacks.
    • Added configuration controls, hot-reload support, runtime status reporting, and a console interface.
    • Added structured execution responses, errors, schemas, and runtime resource safeguards.
  • Documentation

    • Added installation, configuration, API, usage, and end-to-end testing documentation.

One worker exposing `sandbox-code-runner`'s API — `run` /
`register_function` / `teardown` / `inject-guidance` — over two in-process
engines: untrusted JavaScript in deno_core V8 isolates, and untrusted
Python as CPython compiled to WebAssembly inside wasmtime. No microVM and
no /dev/kvm, which is the reason it exists.

The engine internals are two new bus-free libraries, `crates/node-core`
(`iii-node-core`) and `crates/python-core` (`iii-python-core`). Neither
depends on `iii-sdk` at all — not feature-gated, dependency absent — so
neither can collide with the pin the workers carry. `code-runner` owns the
only bus seam and translates core errors into its own taxonomy.

The load-bearing semantic, inherited from `sandbox-code-runner`: a failing
script is a RESPONSE, not an error. A tenant exception returns a resolved
call with `exit_code: 1` and the traceback in `stderr`; errors are reserved
for infrastructure. Host-derived kill signals (`timed_out`, memory,
disk) are checked BEFORE any guest-written byte is parsed, because a tenant
controls every byte under its output directory and could otherwise forge
past an infrastructure kill.

Both languages get a capped private working directory and a guest `iii`
global. Node reaches the host through ops; Python cannot — `python.wasm`
imports one module and exports only `_start` — so its bridge rides
`fd_write`/`fd_read` with sentinel framing, and the host publishes
registered handlers on the guest's behalf. A kept Python runtime persists
its interpreter as well as its files: the wrapper parks on stdin between
calls rather than letting `_start` return, so globals stay bound and
modules stay imported.

Function ids from both engines are claimed in ONE registry, and not under
the bare namespace — that is node's own pre-runtime placeholder owner, and
a python id claimed there is silently reclaimed by node and then aborts the
process from `op_iii_register`'s non-unwinding V8 callback.

366 tests (node-core 217, python-core 86, code-runner 63), fmt and clippy
clean, plus a 24-case e2e suite that runs both languages against a real
engine. Every containment test names the mutation that makes it fail.
Ships two assets into any running console — `code-runner/page.js` over
`console:script` and `code-runner/styles.css` over `console:style` — built
from `ui/` by esbuild and embedded in the binary, so there is nothing to
install and nothing to serve separately. One function-trigger renderer per
op replaces the console's raw-JSON card, which turns `code` into a single
escaped line and buries the verdict.

Ported from `sandbox-code-runner/ui`, which serves the same API, with three
deliberate divergences for this worker's wire:

**`result` gets its own block.** The completion value is the field this
worker's wire adds, and a null one is information rather than an absence
(run.rs never skips it), so it renders explicitly with the engine's return
convention beside it — node code is a function body (`return 2 + 2`),
python code is a module (assign `result`). That mismatch is the usual
reason a call "worked" and came back null, and it was previously invisible.

**No network chip.** `sandbox-code-runner`'s `network` is a real create-time
flag; this worker has no such field because neither engine has any network.
An "off" chip would imply a knob exists, so there is none — asserted by a
test, since the port would otherwise carry it silently.

**In-process wording throughout.** Teardown disposes a V8 isolate or a
CPython interpreter, not a microVM; a python namespace's interpreter is
pinned, so teardown-by-namespace is the only thing that reclaims it.

`runtime_id` is never rendered in full — it is a capability, so it appears
only as a truncated click-to-copy chip, and every other string is filtered
first: stdout, stderr, the error messages that quote it by design, the
submitted source, the completion value, and the `raw json` tab the console
mounts regardless of what a card does.

Adding the UI reopened a process-abort hole, found by probing rather than
assumed: `ConsoleUi` publishes `code-runner::ui-content`, which was not in
`STATIC_IDS` and so was not seeded into the id registry. Because
`register_function` lets a caller choose any namespace — `code-runner::`
included — an unseeded worker id is claimable, and the claim reaches the
SDK's `register_function` on an already-registered id, whose duplicate-id
panic aborts the process from a non-unwinding V8 callback.
`register_function("code-runner::ui-content", …)` returned
`registered: true` before the fix. There is now a `seeded_ids()` that is
strictly larger than `STATIC_IDS`, used by both production and the test
harness so they cannot diverge, and a test that walks every owned id.

72 worker tests (up from 64) and 11 vitest cases; the mutations for the
seeding fix, the result redaction, the null-result block and the
convention hint were each verified to fail their test. fmt, clippy, both CI
validators and biome on `ui/` all clean.
…gine name

Observed live: an agent wrote Node-idiom `global.counter`, got
`ReferenceError: global is not defined`, and the stack frame said
`[node-engine:eval]` — a worker name that no longer exists on the bus.
Sweeping that defect class off every user-visible surface:

- guidance: name the `global`/`globalThis` trap (`lang: "node"` primes
  the Node mental model, and 'NOT Node' alone didn't transfer), and quote
  the REAL id-clash wire code — `code-runner::invalid_request`, not the
  `id_taken` code this worker's taxonomy never had
- script origins: `[code-runner:eval]`/`:prelude`/`:invoke`/`:namespace`,
  with the definition-error frame matcher renamed in lockstep (mutation
  re-verified: a leaking formatCause fails the test on the NEW names)
- prelude: `iii.shutdown()` now points at `code-runner::teardown` (the
  old message named a function nobody can call), `String(iii)` introduces
  a code-runner host client, registerFunction's http refusal ditto
- default namespace mint: `code-runner::<runtime_id>::` — guests were
  publishing live bus ids under the retired prefix
- error seams: NodeEngineError::message() is now pub, and ops + translate
  surface it instead of Display — callers saw double-coded messages like
  `code-runner::invalid_request: node-engine::id_taken: …`; new test pins
  that re-coded messages never carry the core's own prefix
- tenant log stream: the truncation notice, thread names, tempdir prefix
  and tracing target follow

The core's internal `node-engine::<code>` tags stay: after the seam fix
they reach no user surface, and re-coding is the worker's job.
…e-only

Stale since the python registration path landed; the catalog description
steered agents away from a working feature. Golden regenerated — the one
description string, no schema drift.
… to the catalog

engine::functions::info showed REQUEST/RESPONSE "any" for every dynamic
registration: the SDK auto-extracts schemas from the handler's types, and a
dynamic handler is Value → Value. The SDK's escape hatch already existed —
RegisterFunction::request_format/response_format override the extraction —
nothing here reached it.

Two optional wire fields, `request_format`/`response_format` (a superset
of sandbox-code-runner's contract, like `result`), threaded to that
builder:

- python: one hop — the host publishes directly through `Engine::register`,
  which gains the two params (IIIEngine, FakeEngine, TestBus follow)
- node: the whole guest chain — wire → `wrap_register` embeds them as JSON
  literals in the generated `__def` → the prelude's options object →
  `op_iii_register` (two new args, "" = absent) → `Engine::register`
- byproduct: guest code gets the same surface for free —
  `iii.registerFunction(id, h, {request_format, response_format})`

One validation rule at both trust boundaries (wire::register::
validate_format, shared so they cannot drift): a JSON OBJECT carrying at
least one schema-defining keyword (an empty object IS the "any" being
replaced), 16 KiB serialized cap. The worker wire checks before anything is
claimed or booted; the op re-checks because the prelude's own check runs on
tenant-replaceable builtins.

The op's redeploy early-return is extended: it skipped the bus write
whenever the DESCRIPTION was absent, which would silently discard formats —
re-registration is a wholesale metadata replacement, never a merge, and the
comment now says so.

Mutations run for real: node arm dropping the fields from core_req → the
both-languages capture test fails; the swap-branch condition left on
description alone → the redeploy-republishes test fails. The final hop
(IIIEngine's builder calls) has no unit seam — the new e2e case covers it
against a live engine by reading functions::info back.

node-core 227 tests (was 217), code-runner 68+7 (was 66+7); goldens
regenerated (new request fields + REGISTER_DESC sentence, no other drift);
guidance, README and worker catalog updated.
…nventions up front

Two dogfooding findings, both documentation holes:

- request_format/response_format read as a contract but are catalog
  metadata: nothing on this bus validates payloads against them, and an
  agent that reads the schema in functions::info skips its own input
  checks. REGISTER_DESC and the guidance now say so and tell handlers to
  validate.
- the return-vs-result convention was taught only reactively (the null
  card, python's SyntaxError in stderr): the injected guidance mentioned
  python ZERO times, and RUN_DESC never stated either convention, so an
  agent's first python run was written blind. RUN_DESC now names both
  (node = function body, return x; python = module, result = x) and the
  guidance gains its first python clause — lang, conventions, sync iii,
  /work.

Guidance needles pin both clauses; goldens regenerated, descriptions only.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 12, 2026 6:15pm
workers-tech-spec Ready Ready Preview Aug 12, 2026 6:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@andersonleal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32c8478f-ae6e-45ff-8e65-234c9bcaeb1d

📥 Commits

Reviewing files that changed from the base of the PR and between e0a6f9b and b63be01.

📒 Files selected for processing (30)
  • code-runner/README.md
  • code-runner/build.rs
  • code-runner/src/config.rs
  • code-runner/src/configuration.rs
  • code-runner/src/functions/inject_guidance.rs
  • code-runner/src/functions/mod.rs
  • code-runner/src/functions/register.rs
  • code-runner/src/functions/run.rs
  • code-runner/src/main.rs
  • code-runner/src/manager.rs
  • code-runner/src/manifest.rs
  • code-runner/src/truncate.rs
  • code-runner/src/ui.rs
  • code-runner/tests/e2e/code-runner.config.yaml
  • code-runner/tests/e2e/run-tests.sh
  • code-runner/tests/e2e/workers/harness/src/cases-errors.ts
  • code-runner/tests/e2e/workers/harness/src/cases-register.ts
  • code-runner/tests/golden/schemas/code-runner.register_function.json
  • code-runner/tests/golden/schemas/code-runner.run.json
  • code-runner/ui/page.tsx
  • code-runner/ui/src/configuration/index.tsx
  • code-runner/ui/src/lib/shared.tsx
  • code-runner/ui/styles.css
  • crates/node-core/src/wire/register.rs
  • crates/node-core/src/wire/run.rs
  • crates/python-core/src/artifact.rs
  • crates/python-core/src/config.rs
  • crates/python-core/src/manager.rs
  • crates/python-core/src/wrapper.py
  • crates/python-core/tests/wrapper.rs
📝 Walkthrough

Walkthrough

The PR adds a new Rust code-runner worker. It executes Node.js in V8 isolates and Python in CPython-WASM, supports persistent runtimes and function registration, adds configuration reloads and a console UI, and includes extensive unit, schema, integration, and end-to-end tests.

Changes

Worker contracts and bootstrap

Layer / File(s) Summary
Public contracts and startup
code-runner/src/..., code-runner/iii.worker.yaml, code-runner/Cargo.toml, code-runner/build.rs
Adds execution, registration, teardown, language, error, configuration, manifest, and worker startup contracts.
Configuration and registration integration
code-runner/src/configuration.rs, code-runner/src/functions/..., iii-permissions.yaml
Registers functions and schemas, supports hot reload, reserves worker-owned IDs, and adds the internal configuration permission denial.

Node and Python engines

Layer / File(s) Summary
Node engine
crates/node-core/...
Adds isolated V8 execution, guest APIs, registration and trigger operations, scratch files, protocol handling, ID ownership, and resource limits.
Python engine
crates/python-core/...
Adds verified CPython-WASM artifacts, Wasmtime execution, framed persistent interpreters, guest bridges, memory and timeout limits, and runtime recovery.

Worker orchestration

Layer / File(s) Summary
Unified runtime manager
code-runner/src/manager.rs, code-runner/src/translate.rs, code-runner/src/truncate.rs, code-runner/src/node_bus.rs, code-runner/src/python_bus.rs
Routes Node and Python requests, manages runtime ownership and teardown, translates outcomes, caps output, and connects engines to the SDK bus.

Console UI

Layer / File(s) Summary
Embedded console UI
code-runner/ui/*, code-runner/src/ui.rs
Adds the UI build pipeline, configuration form, run/register/teardown renderers, recursive runtime-ID redaction, result rendering, and scoped styles.

Validation

Layer / File(s) Summary
Schema and end-to-end validation
code-runner/tests/*
Adds golden schemas, manifest checks, an executable E2E harness, and tests for execution, persistence, registration, teardown, errors, output limits, and Unicode handling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • iii-hq/workers#728: Adds a closely related Node/Python code-runner surface with matching execution, registration, teardown, guidance, configuration, manager, and UI concepts.
  • iii-hq/workers#579: Establishes similar injectable console UI asset, build, and runtime-registration patterns.
  • iii-hq/workers#425: Uses similar Rust worker configuration, manifest, function registration, CLI, and hot-reload integration patterns.

Suggested labels: no-ticket

Suggested reviewers: ytallo

Poem

I’m a rabbit in a V8 burrow,
With Python wheels tucked safe below.
I run, register, teardown too,
And hide runtime IDs from view.
Fresh schemas bloom, tests hop in—
The code-runner’s ready to begin!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 summarizes the main change: adding an in-process code runner for untrusted Node.js and Python.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/code-runner-worker

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.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 58 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

The committed config.yaml is gone; the configuration worker's code-runner
entry is the authoritative runtime config (--config is a one-time seed).
Output caps and timeouts are load()ed per call from an ArcSwap snapshot the
configuration:updated trigger swaps, so they hot-apply; the engine-structural
fields stay boot-captured and the reload handler warns that they apply at the
next restart. on-config-change is seeded into the runtime-id registry and
denied to agents.
host.configForms.register('code-runner', …) replaces the generic
schema-driven form on the Workers tab: sections carry the reload split
(output caps + timeouts hot-apply on save, runtime/memory/scratch apply at
the next restart), byte fields show KiB/token hints, and the scratch section
computes the worst-case host footprint.
The injected harness guidance already teaches the full surface; the catalog
description now carries contract essentials only (~1/3 the tokens per read)
and no longer points at other workers — the sandbox-code-runner mentions are
gone from RUN_DESC and the Lang schema doc.
inject_guidance (default true, hot-apply) silences the pre-generate guidance
hook: disabled, the handler answers with the no-op mutation so the harness
prompt is untouched. The hook stays bound — one no-op roundtrip per
generation while off. Toggle ships in the console config form under a new
agent-guidance section.
each_turn_is_budgeted_on_its_own_timeout gave the first turn 600ms, which a
trivial turn on a slow shared CI runner blows (3/3 CI failures; passes on a
fast local box). First-turn budget 600ms -> 3s and the second turn's sleep
1.5s -> 4s, preserving the discrimination: the sleep still exceeds the
first-turn budget, so a leaked budget still trips the assertion.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (12)
code-runner/src/functions/run.rs-8-10 (1)

8-10: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the language-specific execution contract.

Line 8 states that code runs as a whole file. Lines 8-10 state that every runtime uses iii.files. Line 21 instead describes Node as an async function body and Python as a module with /work instead of iii.files. These public contracts cannot both be correct.

  • code-runner/src/functions/run.rs#L8-L10: document the actual Node and Python execution and filesystem behavior separately.
  • code-runner/src/functions/inject_guidance.rs#L21-L21: match the same contract and add coverage for the filesystem and execution wording.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/src/functions/run.rs` around lines 8 - 10, Align the public
execution-contract documentation in code-runner/src/functions/run.rs lines 8-10
and code-runner/src/functions/inject_guidance.rs line 21: describe Node as an
async function body and Python as a module, and document each language’s actual
filesystem behavior consistently instead of claiming all runtimes use iii.files.
Add or update coverage for both the execution and filesystem wording, with both
files expressing the same contract.
code-runner/src/functions/register.rs-8-10 (1)

8-10: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document namespace ownership per language.

code-runner/src/manager.rs supports Node and Python runtimes under the same namespace. Line 10 instead says later IDs must share the first registration language. Lines 40-42 and line 21 describe one runtime per namespace. This prevents callers from using a supported registration layout.

  • code-runner/src/functions/register.rs#L8-L10: state that a namespace can contain registrations for both languages.
  • code-runner/src/functions/mod.rs#L39-L45: replace “one persistent runtime per namespace” with one runtime per (namespace, lang).
  • code-runner/src/functions/inject_guidance.rs#L21-L21: describe the same per-language runtime model and namespace teardown behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/src/functions/register.rs` around lines 8 - 10, Update the
documentation in code-runner/src/functions/register.rs lines 8-10 to state that
a namespace may contain registrations for both supported languages. In
code-runner/src/functions/mod.rs lines 39-45, describe runtime ownership as one
runtime per (namespace, lang), not one per namespace. In
code-runner/src/functions/inject_guidance.rs line 21, document the same
per-language runtime model and clarify namespace teardown behavior.
code-runner/README.md-59-59 (1)

59-59: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify a language for both fenced code blocks.

Markdownlint reports MD040 for these fences. Use text for the output examples.

Also applies to: 78-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/README.md` at line 59, Update both fenced code blocks in the
README to specify the text language by adding the text fence identifier,
including the output example near the referenced second location, while leaving
their contents unchanged.

Source: Linters/SAST tools

code-runner/build.rs-23-31 (1)

23-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Track packages/console-ui when deciding whether to rebuild UI assets.

ui/package.json links the workspace package, but neither invalidation path watches it. A change in packages/console-ui can leave stale page.js embedded in the worker.

Add the package directory to cargo:rerun-if-changed and to dist_is_fresh.

Proposed fix
 println!("cargo:rerun-if-changed=ui/package.json");
+println!("cargo:rerun-if-changed=../packages/console-ui");
 println!("cargo:rerun-if-changed=../pnpm-lock.yaml");
-    for dir in [ui_dir.join("src")] {
+    for dir in [
+        ui_dir.join("src"),
+        ui_dir.join("../../packages/console-ui"),
+    ] {

Also applies to: 107-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/build.rs` around lines 23 - 31, Update the build script’s UI
change tracking to include the packages/console-ui directory: add it to the
cargo:rerun-if-changed declarations and include the same path in dist_is_fresh
so changes to the linked workspace package trigger rebuilding embedded UI
assets.
code-runner/tests/e2e/workers/harness/src/cases-errors.ts-57-59 (1)

57-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the configured stream cap.

Line 58 states that each stream has a 1 MiB cap. Line 59 accepts up to 2 MiB. A regression that doubles the stdout cap passes this test. Set the bound to 1 MiB, plus a defined truncation-marker allowance if the response adds one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/e2e/workers/harness/src/cases-errors.ts` around lines 57 -
59, Update the stdout length assertion in the error-case harness to enforce the
stated 1 MiB stream cap, allowing only a clearly defined additional amount if
truncation adds a marker. Keep the assertion aligned with the cap described by
the surrounding comment so a doubled stdout limit fails.
code-runner/tests/e2e/run-tests.sh-121-121 (1)

121-121: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the missing-result diagnostic.

At Line 121, grep exits with status 1 when the harness produces no HARNESS_DONE line. With errexit enabled, the script exits before Lines 125-127 print the intended failure message.

Proposed fix
-SUITE_LINE="$(grep -a 'HARNESS_DONE' "$LOGS/harness.log" | tail -1)"
+SUITE_LINE="$(grep -a 'HARNESS_DONE' "$LOGS/harness.log" | tail -1 || true)"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/e2e/run-tests.sh` at line 121, Update the HARNESS_DONE
lookup in the test script so a missing match does not trigger errexit before the
diagnostic handling at Lines 125-127 runs. Preserve the existing SUITE_LINE
assignment and ensure the no-result path reaches the intended failure message.
code-runner/tests/e2e/workers/harness/src/cases-register.ts-142-145 (1)

142-145: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the schema assertion effective.

After Line 144 passes, raw.includes('fmt_marker_n') is true. Therefore, Line 145 always passes and does not detect an "any" schema. Assert the selected catalog entry has the expected request schema and does not retain the fallback schema.

Proposed fix
-        expect(!raw.includes('"any"') || raw.includes('fmt_marker_n'), 'schema should replace any')
+        expect(!raw.includes('"any"'), 'schema should replace any')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/e2e/workers/harness/src/cases-register.ts` around lines 142
- 145, Update the assertions in the test case around the engine info request so
they inspect the selected catalog entry’s request schema directly. Assert that
the schema contains the expected fmt_marker_n value and separately assert that
it does not contain the fallback "any" schema; do not combine the checks with an
OR that becomes true whenever fmt_marker_n is present.
crates/node-core/src/wire/run.rs-21-30 (1)

21-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Retired node-engine name reaches callers through the derived schemas. These doc comments become field descriptions in the schemars-derived JSON Schema, and code-runner/src/functions/mod.rs publishes those schemas in the worker catalog. This PR retires node-engine, so callers read a worker name and a function id that no longer exist. The internal node-engine::<code> error strings in crates/node-core/src/error.rs are a separate, documented decision and are out of scope here.

  • crates/node-core/src/wire/run.rs#L21-L30: replace node-engine::teardown with the id the worker actually registers, and correct the stated default namespace prefix node-engine::<runtime_id>:: after confirming the value manager.rs produces.
  • crates/node-core/src/wire/register.rs#L6-L9: replace "node-engine keeps one runtime per namespace" with the code-runner worker name.

Regenerate the golden schemas under code-runner/tests/golden/schemas/ after the edit, because the description text is part of those fixtures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/node-core/src/wire/run.rs` around lines 21 - 30, Update the field
documentation in crates/node-core/src/wire/run.rs#L21-L30 to use the actual
registered code-runner worker name and function id instead of retired
node-engine identifiers, and verify the namespace prefix against manager.rs
before correcting it; update crates/node-core/src/wire/register.rs#L6-L9 to name
the code-runner worker. Regenerate the golden schemas under
code-runner/tests/golden/schemas/ so the published descriptions match.
code-runner/src/ui.rs-131-134 (1)

131-134: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This assertion matches more than a bare react import.

The raw string r#"from "react"# ends before the closing quote, so it matches from "react. It also matches from "react-dom/client" and from "react-is". A bundle that imports only react-dom while inlining React source would pass. Match the full specifier.

Proposed fix
-        assert!(
-            PAGE_JS.contains(r#"from "react"#),
-            "react should be imported, not bundled"
-        );
+        assert!(
+            PAGE_JS.contains(r#"from "react""#),
+            "react should be imported, not bundled"
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/src/ui.rs` around lines 131 - 134, Update the PAGE_JS assertion
to match the complete React module specifier, including the closing quote, so
imports such as react-dom/client or react-is do not satisfy it; keep the
assertion anchored to the existing “react should be imported, not bundled”
check.
code-runner/src/manager.rs-546-569 (1)

546-569: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A registered python handler pins its timeout at registration time.

py_handler reads self.cfg.load().default_timeout_ms once, outside the closure, and captures the value. A later configuration:updated swap does not reach handlers that are already registered. The struct doc on Lines 38-42 states that per-call knobs, including timeouts, are load()ed at each use so they hot-reload. py_define_handler follows that rule; this path does not.

Proposed fix: read the snapshot per invocation
     fn py_handler(&self, python: &Arc<PythonManager>, runtime_id: &str) -> ProxyHandler {
         let python = python.clone();
         let runtime_id = runtime_id.to_string();
-        let timeout_ms = self.cfg.load().default_timeout_ms;
+        let cfg = self.cfg.clone();
         Arc::new(move |payload: serde_json::Value| {
             let python = python.clone();
             let runtime_id = runtime_id.clone();
+            let timeout_ms = cfg.load().default_timeout_ms;
             Box::pin(async move {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/src/manager.rs` around lines 546 - 569, Update py_handler so
default_timeout_ms is loaded from self.cfg for each handler invocation rather
than captured during registration. Capture or clone the configuration handle in
the closure, then read the current snapshot inside the async call when
constructing RunRequest; preserve the existing timeout and result/error
behavior.
crates/python-core/src/manager.rs-548-566 (1)

548-566: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The byte cap does not count the appended newlines.

The check at Line 554 compares kept.len() + line.len() against MAX_LOG_BYTES, but Line 559 appends a '\n' that the check never accounts for. A stream of many short lines can therefore exceed MAX_LOG_BYTES by up to MAX_LOG_LINES bytes. text.lines() also strips the original line terminators, so a stream that ends without a newline gains one.

🐛 Proposed fix
         for (n, line) in text.lines().enumerate() {
-            if n >= MAX_LOG_LINES || kept.len() + line.len() > MAX_LOG_BYTES {
+            if n >= MAX_LOG_LINES || kept.len() + line.len() + 1 > MAX_LOG_BYTES {
                 truncated = true;
                 break;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/manager.rs` around lines 548 - 566, Update the cap
closure in capture_streams to include the appended newline in the MAX_LOG_BYTES
check, including for the final unterminated line produced by text.lines().
Preserve the existing MAX_LOG_LINES limit and set truncated when adding the line
plus its newline would exceed the byte cap.
crates/python-core/src/artifact.rs-31-39 (1)

31-39: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Do not panic when HOME is unset.

ensure_extracted runs on the worker boot path. If neither XDG_CACHE_HOME nor HOME is set, expect panics and aborts the worker instead of returning an error. Containers frequently run without HOME. Make cache_root fall back to a temporary directory, or return Result so the caller reports a normal engine start failure.

🛡️ Proposed fix
 fn cache_root() -> PathBuf {
     let base = std::env::var_os("XDG_CACHE_HOME")
         .map(PathBuf::from)
-        .unwrap_or_else(|| {
-            let home = std::env::var_os("HOME").expect("HOME must be set");
-            PathBuf::from(home).join(".cache")
-        });
+        .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
+        .unwrap_or_else(std::env::temp_dir);
     base.join("iii").join("python-engine").join(ZIP_SHA256)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/artifact.rs` around lines 31 - 39, Update cache_root
so it does not call expect when HOME is unset. Prefer falling back to an
appropriate temporary directory when neither XDG_CACHE_HOME nor HOME is
available, while preserving the existing cache subpath; alternatively, change
cache_root and its ensure_extracted caller to return and propagate a normal
error instead of panicking.
🧹 Nitpick comments (17)
code-runner/ui/src/configuration/index.tsx (1)

43-49: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

bytesHint reports ~0 tokens for small non-zero caps.

Math.round(bytes / 4 / 100) * 100 rounds to zero for any cap below 200 bytes. An operator who sets max_stream_bytes to 128 then reads "≈0.1 KiB (~0 tokens)", which reads as "off" even though the cap is active. Consider a floor of 100 tokens for non-zero caps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/ui/src/configuration/index.tsx` around lines 43 - 49, Update
bytesHint so any non-zero byte cap reports at least 100 tokens, while preserving
the existing rounded estimate for larger caps and the special “off” message for
zero bytes.
code-runner/ui/page.tsx (1)

25-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collect the configForms remover in the teardown.

host.configForms.register returns a remover (packages/console-ui/index.d.ts:201-226). Line 27 discards it, so the early teardown removes the three renderers but leaves the code-runner config form registered. If the loader disposal is the only cleanup for the form, the returned function is inconsistent with its own comment.

♻️ Proposed fix
 export default function setup(host: Host) {
-  const removers = createCodeRunnerRenderers(host).map((renderer) => host.functionTriggers.register(renderer))
-
-  host.configForms.register('code-runner', CodeRunnerConfigForm)
+  const removers = createCodeRunnerRenderers(host).map((renderer) => host.functionTriggers.register(renderer))
+
+  removers.push(host.configForms.register('code-runner', CodeRunnerConfigForm))
 
   // The loader already disposes every registration; returning the removers
   // makes an early teardown (hot reload mid-session) explicit and ordered.
   return () => {
     for (const remove of removers) remove()
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/ui/page.tsx` around lines 25 - 33, Capture the remover returned
by host.configForms.register in the setup flow alongside the renderer removers,
then invoke it during the returned teardown so both renderer registrations and
the code-runner config form are removed in order.
code-runner/ui/styles.css (1)

64-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the infinite pulse animation with prefers-reduced-motion.

.cr-ui-msg-note.pulse animates opacity forever while a call is in flight. Users who request reduced motion get no way to stop it.

♻️ Proposed fix
 `@keyframes` cr-ui-pulse {
   0%, 100% { opacity: 1 }
   50% { opacity: 0.35 }
 }
+@media (prefers-reduced-motion: reduce) {
+  [data-iii-ui="code-runner"] .cr-ui-msg-note.pulse {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/ui/styles.css` around lines 64 - 70, Guard the infinite animation
on .cr-ui-msg-note.pulse with a prefers-reduced-motion media query so users who
request reduced motion do not receive the pulse effect. Keep the existing
cr-ui-pulse animation unchanged for users without that preference.
code-runner/ui/src/lib/shared.tsx (1)

147-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the textarea on the execCommand failure path.

If ta.select() or document.execCommand('copy') throws, line 165 never runs and the hidden textarea stays in document.body. Every failed copy then adds one node. Move the removal into a finally block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/ui/src/lib/shared.tsx` around lines 147 - 170, Update copyText so
the temporary textarea created in the fallback path is always removed, including
when ta.select() or document.execCommand('copy') throws. Move
document.body.removeChild(ta) into a finally block while preserving the existing
success result and false-on-error behavior.
code-runner/tests/e2e/workers/harness/src/cases-run.ts (1)

27-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert that the run spec was found.

If engine::functions::info returns entries without a function_id field, find returns undefined and line 29 throws a TypeError. The case then reports a property-access error instead of a readable assertion.

♻️ Proposed guard
       const run = found.find((f: any) => f.function_id === 'code-runner::run')
+      expect(!!run, `functions::info returned no entry for code-runner::run: ${JSON.stringify(found)}`)
       expect(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/e2e/workers/harness/src/cases-run.ts` around lines 27 - 31,
Assert that the `run` result from `found.find(...)` exists before accessing
`run.request_schema`, using a clear failure message; keep the existing
required-`code` assertion for the found spec unchanged.
code-runner/tests/schemas.rs (1)

100-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse support::golden_root() and golden_file_name for the on-disk checks.

Lines 106, 107, and 131 hardcode CWD-relative paths and repeat the ::. mapping already implemented at line 13. The paths resolve only because Cargo sets the test working directory to the package root.

♻️ Proposed refactor
-    assert!(std::path::Path::new("tests/golden/schemas/code-runner.run.json").exists());
-    assert!(!std::path::Path::new("tests/golden/schemas/code-runner.eval.json").exists());
+    assert!(support::golden_root().join(golden_file_name("code-runner::run")).exists());
+    assert!(!support::golden_root().join(golden_file_name("code-runner::eval")).exists());
-        let golden = format!("tests/golden/schemas/{}.json", gone.replace("::", "."));
+        let golden = support::golden_root().join(golden_file_name(gone));
         assert!(
-            !std::path::Path::new(&golden).exists(),
-            "{golden} still exists on disk"
+            !golden.exists(),
+            "{} still exists on disk",
+            golden.display()
         );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/schemas.rs` around lines 100 - 137, Update the on-disk
golden checks in the_advertised_ids_agree_across_catalog_static_ids_and_goldens
and removed_functions_are_absent_from_the_catalog to use support::golden_root()
with golden_file_name instead of hardcoded CWD-relative paths and manual "::" to
"." conversion. Preserve the existing existence and absence assertions.
code-runner/tests/e2e/workers/harness/src/runner.ts (1)

76-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the TTY color constants into one shared helper. Both files derive the same ANSI escapes from process.stdout.isTTY, so the palette is maintained twice.

  • code-runner/tests/e2e/workers/harness/src/runner.ts#L76-L80: move this block into a small exported helper (for example colors() in cases.ts or a new colors.ts) and consume it here.
  • code-runner/tests/e2e/workers/harness/src/worker.ts#L19-L22: import the same helper instead of redeclaring GREEN, RED, and RESET.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/tests/e2e/workers/harness/src/runner.ts` around lines 76 - 80,
Extract the shared TTY-based ANSI palette from runner.ts lines 76-80 into one
exported helper, such as colors(), and update runner.ts to consume it. In
worker.ts lines 19-22, import and use the same helper instead of redeclaring
GREEN, RED, and RESET; preserve the current non-TTY empty-string behavior.
crates/node-core/src/ids.rs (1)

36-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the lock acquisitions panic-free.

The module doc states that a panic reaching an extern "C" op callback aborts the process and kills every tenant's runtime. .lock().unwrap() panics if the mutex is poisoned, so the current code depends on no critical section ever panicking. Recover the guard instead. The map stays consistent, because every critical section here is a complete insert, remove, or retain.

♻️ Proposed change to recover a poisoned guard
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
+
+/// A poisoned guard is still a consistent map: every critical section below
+/// is one complete insert, remove or retain. Recovering beats `unwrap`,
+/// which would panic out of `op_iii_register` and abort the process.
+fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
+    m.lock().unwrap_or_else(PoisonError::into_inner)
+}
     pub fn claim(&self, id: &str, owner: &str) -> bool {
-        let mut map = self.0.lock().unwrap();
+        let mut map = lock(&self.0);

Apply the same substitution in claim_all, release_ids, release_owner, and the Debug impl.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/node-core/src/ids.rs` around lines 36 - 85, Replace every
`.lock().unwrap()` in the IDs implementation, including `claim`, `claim_all`,
`release_ids`, `release_owner`, and the `Debug` implementation, with
poisoned-mutex recovery that extracts and reuses the inner guard. Preserve the
existing critical-section behavior and return values while ensuring lock
acquisition cannot panic.
crates/node-core/src/error.rs (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the module documentation with the conversion path

code-runner passes recoded errors through err.message(), not Error::Handler(e.to_string()). Update the module documentation to describe the actual translate::node_err path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/node-core/src/error.rs` around lines 1 - 9, Update the module-level
documentation in error.rs to describe the actual translate::node_err conversion
path used by code-runner, including that recoded errors pass through
err.message(). Remove the inaccurate reference to Error::Handler(e.to_string())
while preserving the stable wire-code taxonomy description.
code-runner/src/truncate.rs (1)

160-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen this test's assertions.

capped is a String, so is_char_boundary(0) and from_utf8(...).is_ok() are always true. The test still catches a boundary bug, because slicing at a non-boundary panics, but the assertions themselves prove nothing. Assert the observable contract instead: the marker is present, and the head and tail hold whole é characters.

Proposed stronger assertions
     let s: String = "é".repeat(20_000); // 2-byte chars
     let capped = cap_stream("stdout", s, 16_384);
-    assert!(capped.is_char_boundary(0) && std::str::from_utf8(capped.as_bytes()).is_ok());
+    let (head, rest) = capped.split_once("\n[…stdout").expect("marker present");
+    let tail = rest.split_once("]\n").expect("marker closed").1;
+    assert!(head.chars().all(|c| c == 'é'), "head lost a character");
+    assert!(tail.chars().all(|c| c == 'é'), "tail lost a character");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code-runner/src/truncate.rs` around lines 160 - 165, Strengthen
stream_truncation_respects_utf8_boundaries by removing the tautological UTF-8
checks and asserting cap_stream’s observable output: verify the truncation
marker is present, and confirm both the retained head and tail consist of
complete é characters rather than partial bytes.
crates/python-core/tests/runner.rs (1)

69-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The assertion depends on Wasmtime's trap text.

msg.contains("wasm trap: interrupt") couples this test to a Wasmtime display string. The exact pin wasmtime = "=47.0.3" protects it today, but a pin bump can break the test for a wording change rather than a behavior change. If ExitKind::Trap can carry a structured cause, match on that instead. Otherwise add a comment that links the assertion to the pin.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/tests/runner.rs` around lines 69 - 75, Update the
ExitKind::Trap assertion in the runner test to validate a structured trap cause
instead of matching Wasmtime’s display text, if that information is available.
If ExitKind::Trap only exposes the rendered message, add a concise comment
documenting its dependency on the exact wasmtime = "=47.0.3" pin.
crates/python-core/src/wrapper.py (2)

46-52: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

A malformed bridge config kills the run before any envelope is written.

Line 47 catches only OSError. If iii.json is present but truncated, json.load raises JSONDecodeError, and Line 52 raises KeyError when sentinel is absent. Both escape main, so the guest exits without writing /out/result.json, and the host reports an unattributable exit. The host writes this file, so the case is not tenant-reachable, but a partial write during a crash is possible. Catch (OSError, ValueError, KeyError) and treat it as "no bridge".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/wrapper.py` around lines 46 - 52, Update the bridge
config loading in main to catch OSError, ValueError, and KeyError from opening,
parsing, or reading cfg["sentinel"], and return (False, None) for any malformed
or unavailable iii.json so the run proceeds as “no bridge.”

21-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

write_error does not cap tb_text.

The module comment states that the cap lives here "so no future caller can forget", but only message is capped. tb_text is truncated at the call site at Line 136. A future caller that passes an untruncated traceback builds an envelope the host may refuse. Move the traceback cap into write_error.

♻️ Proposed refactor
 def write_error(out_dir, kind, message, tb_text=None):
     if len(message) > MAX_MESSAGE_CHARS:
         message = message[:MAX_MESSAGE_CHARS] + "... message truncated"
+    if tb_text is not None and len(tb_text) > MAX_TRACEBACK_CHARS:
+        tb_text = tb_text[:MAX_TRACEBACK_CHARS] + "\n... traceback truncated"
     with open(out_dir + "/result.json", "w") as f:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/wrapper.py` around lines 21 - 25, Update write_error
to apply MAX_MESSAGE_CHARS truncation to tb_text before serializing result.json,
using the same truncation behavior as message. Remove the redundant traceback
truncation at the caller so write_error is the single enforcement point for all
traceback inputs.
crates/python-core/src/manager.rs (1)

399-438: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

work_dir is always None at the only call site.

run is the sole caller of run_with, and it passes None. The parameter adds an unused path through validation and permit acquisition. If no other caller is planned, inline the body into run and drop the parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/manager.rs` around lines 399 - 438, The run_with
method receives work_dir as None from its sole caller, so remove the unnecessary
parameter and inline its validation, permit acquisition, RunSpec construction,
runner invocation, and classify flow into run. Update the run call path
accordingly while preserving the existing behavior and error handling.
crates/python-core/tests/wrapper.rs (1)

176-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse run_wrapper instead of duplicating its setup.

Lines 186-205 repeat the whole harness from run_wrapper only to learn out_dir before the run. Give run_wrapper an optional placeholder substitution, or return the directories, and the duplicate block disappears.

♻️ Sketch
-fn run_wrapper(code: &str, payload: Option<&str>) -> Run {
+/// `code` may contain the literal `OUT`, which is replaced with the real
+/// output directory before the run.
+fn run_wrapper(code: &str, payload: Option<&str>) -> Run {
     let dir = tempfile::tempdir().unwrap();
     let run_dir = dir.path().join("run");
     let out_dir = dir.path().join("out");
     fs::create_dir_all(&run_dir).unwrap();
     fs::create_dir_all(&out_dir).unwrap();
     fs::write(run_dir.join("main.py"), WRAPPER).unwrap();
-    fs::write(run_dir.join("code.py"), code).unwrap();
+    fs::write(
+        run_dir.join("code.py"),
+        code.replace("OUT", out_dir.to_str().unwrap()),
+    )
+    .unwrap();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/tests/wrapper.rs` around lines 176 - 210, Refactor
envelope_is_written_after_tenant_code_finishes to reuse the existing run_wrapper
test helper instead of duplicating temporary-directory creation, wrapper setup,
Python lookup, command execution, and output parsing. Extend run_wrapper with
the minimal optional placeholder-substitution or directory-return capability
needed for the tenant code to target out_dir, while preserving the test’s
assertion that the final envelope contains “genuine”.
crates/python-core/tests/artifact.rs (1)

26-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The test name claims a cache hit that the assertions do not prove.

The second load_module call only asserts success. A regression that ignored the persisted cwasm and recompiled would still pass. Compare the file modification time, or measure that the second load is materially faster, to make the cache-hit claim observable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/tests/artifact.rs` around lines 26 - 40, Update the test
module_compiles_and_second_load_hits_cwasm_cache to record the persisted cwasm
modification time after the first load, then verify after the second load that
the file remains unchanged, proving the cached artifact was reused rather than
rewritten by recompilation.
crates/python-core/src/config.rs (1)

17-49: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider validating max_concurrent_runs and max_runtimes against zero as well.

Semaphore::new(0) in Manager::new permits nothing, so a config of max_concurrent_runs: 0 makes every run hang until shutdown rather than fail. A max_runtimes: 0 makes every create_runtime return Capacity. A custom Deserialize check, or a validate() called at load time, turns both into a loud startup error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/python-core/src/config.rs` around lines 17 - 49, Validate
PythonEngineConfig values so max_concurrent_runs and max_runtimes must both be
greater than zero before Manager::new or runtime creation uses them. Add the
check through the existing deserialization or configuration-loading path,
ensuring invalid zero values produce a startup error rather than silently
disabling runs or capacity.
🔇 Additional comments (58)
code-runner/ui/build.mjs (1)

26-47: LGTM!

code-runner/ui/package.json (1)

1-23: LGTM!

code-runner/ui/src/function-trigger-message/index.tsx (1)

43-47: LGTM!

code-runner/ui/src/function-trigger-message/register-function.tsx (2)

113-137: LGTM!

Also applies to: 214-258, 283-305


90-100: 🎯 Functional Correctness

Do not change str; TypeScript 5.9.3 narrows obj[k] for this unmodified parameter.

			> Likely an incorrect or invalid review comment.
code-runner/ui/src/function-trigger-message/result.test.tsx (1)

37-43: LGTM!

Also applies to: 85-187

code-runner/ui/src/function-trigger-message/run.tsx (1)

94-103: LGTM!

Also applies to: 154-182, 201-240, 278-334

code-runner/ui/src/function-trigger-message/teardown.tsx (1)

60-90: LGTM!

Also applies to: 92-137, 168-212

code-runner/ui/src/lib/shared.tsx (1)

88-145: LGTM!

Also applies to: 306-349, 361-394

code-runner/ui/tsconfig.json (1)

2-13: LGTM!

code-runner/ui/styles.css (1)

27-32: 📐 Maintainability & Code Quality

No token changes needed.

The console defines all listed tokens, including --color-ring and --color-rule-2.

code-runner/iii.worker.yaml (1)

1-29: LGTM!

code-runner/src/configuration.rs (1)

1-191: LGTM!

code-runner/src/error.rs (1)

1-167: LGTM!

code-runner/src/lib.rs (1)

1-20: LGTM!

code-runner/src/main.rs (1)

1-74: LGTM!

Also applies to: 76-93, 99-149, 154-180

code-runner/tests/e2e/workers/harness/src/cases-run.ts (1)

35-112: LGTM!

code-runner/tests/e2e/workers/harness/src/groups.ts (1)

1-13: LGTM!

code-runner/tests/e2e/workers/harness/src/runner.ts (1)

26-73: LGTM!

Also applies to: 82-111

code-runner/tests/golden/schemas/code-runner.inject-guidance.json (1)

1-56: LGTM!

code-runner/tests/golden/schemas/code-runner.register_function.json (1)

1-75: LGTM!

code-runner/tests/golden/schemas/code-runner.run.json (1)

1-106: LGTM!

code-runner/tests/golden/schemas/code-runner.teardown.json (1)

1-61: LGTM!

code-runner/tests/manifest.rs (1)

5-31: LGTM!

code-runner/tests/schemas.rs (1)

12-94: LGTM!

code-runner/tests/support/mod.rs (1)

17-118: LGTM!

code-runner/tests/e2e/workers/harness/tsconfig.json (1)

10-10: 📐 Maintainability & Code Quality

No change needed. code-runner/tests/e2e/workers/harness/package.json declares @types/node in devDependencies, so "types": ["node"] is resolvable for this harness.

			> Likely an incorrect or invalid review comment.
crates/node-core/src/allocator.rs (1)

56-95: LGTM!

Also applies to: 102-126, 142-163, 180-190

crates/node-core/src/config.rs (1)

11-60: LGTM!

Also applies to: 87-115

crates/node-core/src/lib.rs (1)

11-20: LGTM!

crates/node-core/src/protocol.rs (1)

17-34: LGTM!

Also applies to: 38-67, 157-239

crates/node-core/src/wire/mod.rs (1)

10-12: LGTM!

crates/node-core/src/wire/teardown.rs (1)

5-15: 🎯 Functional Correctness

No change needed: exactly-one validation is enforced. RuntimeManager::teardown returns invalid_request when both selectors are absent or both are present.

			> Likely an incorrect or invalid review comment.
crates/node-core/Cargo.toml (1)

28-29: 📐 Maintainability & Code Quality

Confirm that deno_core = "=0.409.0" exposes the required v8 allocator APIs.

crates/node-core/src/prelude.js (2)

443-472: new Function here is the worker's purpose: it compiles guest source inside a V8 isolate whose escape surface is the iii ops. The static analysis code-injection findings on Lines 445 and 452 do not apply to this design.

Source: Linters/SAST tools


1-128: LGTM!

Also applies to: 130-205, 206-263, 265-331, 333-413, 415-514, 516-552

crates/node-core/src/engine.rs (1)

14-153: LGTM!

Also applies to: 155-518, 520-621

crates/node-core/src/ops.rs (2)

690-710: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

A cancelled op_iii_register_trigger leaks a pending_registrations slot.

pending_registrations is incremented before the await and decremented only after it returns. If the op future is dropped before completion — for example when the watchdog terminates the eval while the runtime is kept for later calls — the counter stays high. The runtime then holds a permanently reduced share of MAX_REGISTRATIONS_PER_RUNTIME for its whole life. inflight_calls documents that a leaked counter "dies with the OpsState it lives in", which holds for a one-shot runtime but not for a kept one.

Consider releasing the reservation from a drop guard on the isolate thread, or recomputing the reservation count from a per-call token list.


18-171: LGTM!

Also applies to: 173-250, 251-362, 364-428, 430-610, 612-758, 760-928, 930-1036, 1038-1103, 1105-1279, 1281-1308, 1310-1688

crates/node-core/tests/golden/iii-surface.txt (1)

1-13: LGTM!

code-runner/src/node_bus.rs (2)

179-186: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

The result of register_trigger_type is discarded.

let _ = self.iii.register_trigger_type(...) drops whatever the SDK returns. If that value carries a failure, the caller still receives success: op_iii_register_trigger_type has already claimed the id, pushed an entry into unregisters, and reported the type in registered. The guest then believes a trigger type is published while the bus holds nothing.

If the SDK returns a Result, propagate the failure instead. Confirm the return type before changing the code.


1-95: LGTM!

Also applies to: 96-178, 188-341

code-runner/src/python_bus.rs (2)

33-53: 🩺 Stability & Availability | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Confirm that timeout_ms is clamped before it reaches this bridge.

The node side clamps a guest-supplied timeout to max_timeout_ms inside op_iii_call. This bridge forwards timeout_ms unchanged. If guest Python can influence the value through its own iii.trigger, a guest can request an engine call far longer than the configured ceiling.


1-31: LGTM!

crates/python-core/build.rs (1)

6-24: LGTM!

Also applies to: 26-62

code-runner/src/manager.rs (1)

30-137: LGTM!

Also applies to: 139-259, 261-304, 315-395, 419-456, 458-492, 494-545, 571-590, 592-726, 728-742, 744-1724

code-runner/src/translate.rs (1)

26-105: LGTM!

Also applies to: 107-204, 206-278, 280-341

code-runner/src/truncate.rs (1)

30-55: LGTM!

Also applies to: 57-93, 95-123, 129-158

code-runner/src/ui.rs (1)

34-62: LGTM!

Also applies to: 68-130, 137-174

crates/python-core/Cargo.toml (1)

41-41: 📐 Maintainability & Code Quality | ⚡ Quick win

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify that these major versions exist as stable releases.

zip = "8", sha2 = "0.11", and which = "8" request major versions that were not stable at my knowledge cutoff. sha2 in particular had a long 0.11.0-pre.* series, and a caret requirement does not resolve pre-release versions. Confirm each resolves to a published stable release.

Also applies to: 47-47, 50-50

crates/python-core/src/artifact.rs (1)

13-13: 🎯 Functional Correctness | 💤 Low value

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify that the sidecar digest file carries no trailing newline.

ZIP_SHA256 is embedded with include_str! and then used as a filesystem path component at Line 38. If build.rs writes the digest with a trailing newline, the cache directory name contains that newline, and the equality assertion in crates/python-core/tests/artifact.rs against the bare hex literal fails. build.rs is not in this review context. Confirm the sidecar content, or trim it at use time.

Also applies to: 38-38

crates/python-core/src/error.rs (1)

7-104: LGTM!

crates/python-core/src/lib.rs (1)

8-12: LGTM!

crates/python-core/src/manager.rs (1)

265-294: LGTM!

Also applies to: 314-327, 336-341

crates/python-core/tests/manager.rs (1)

11-37: LGTM!

Also applies to: 39-233, 235-273, 280-436, 444-476, 481-661

crates/python-core/tests/persistence.rs (1)

11-45: LGTM!

Also applies to: 48-146, 152-209, 223-302, 306-426

crates/python-core/tests/runner.rs (1)

26-49: LGTM!

Also applies to: 89-125, 142-194, 196-230, 232-273, 278-325, 331-400, 406-439

crates/python-core/tests/wrapper.rs (1)

17-48: 📐 Maintainability & Code Quality | 💤 Low value

⚠️ Unverified finding
Sandbox verification was unavailable.

Verify that the host python3 version matches the bundled interpreter.

These tests execute wrapper.py with the host python3, but production runs it under the bundled CPython-WASM build. crates/python-core/tests/artifact.rs asserts a lib/python3.14 path, so the bundle is 3.14. A host on an older minor version can accept or reject wrapper syntax differently, which makes this suite pass while the real guest fails. Consider asserting a minimum host version, or documenting the accepted drift.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ee799a3-925c-4a7f-90de-487036421eef

📥 Commits

Reviewing files that changed from the base of the PR and between 4286bec and e0a6f9b.

⛔ Files ignored due to path filters (5)
  • code-runner/Cargo.lock is excluded by !**/*.lock
  • code-runner/tests/e2e/workers/harness/package-lock.json is excluded by !**/package-lock.json
  • crates/node-core/Cargo.lock is excluded by !**/*.lock
  • crates/python-core/Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (92)
  • README.md
  • code-runner/Cargo.toml
  • code-runner/README.md
  • code-runner/build.rs
  • code-runner/iii.worker.yaml
  • code-runner/src/config.rs
  • code-runner/src/configuration.rs
  • code-runner/src/error.rs
  • code-runner/src/functions/inject_guidance.rs
  • code-runner/src/functions/mod.rs
  • code-runner/src/functions/register.rs
  • code-runner/src/functions/run.rs
  • code-runner/src/functions/teardown.rs
  • code-runner/src/lang.rs
  • code-runner/src/lib.rs
  • code-runner/src/main.rs
  • code-runner/src/manager.rs
  • code-runner/src/manifest.rs
  • code-runner/src/node_bus.rs
  • code-runner/src/python_bus.rs
  • code-runner/src/translate.rs
  • code-runner/src/truncate.rs
  • code-runner/src/ui.rs
  • code-runner/tests/e2e/.gitignore
  • code-runner/tests/e2e/README.md
  • code-runner/tests/e2e/code-runner.config.yaml
  • code-runner/tests/e2e/config.yaml
  • code-runner/tests/e2e/reports/.gitkeep
  • code-runner/tests/e2e/run-tests.sh
  • code-runner/tests/e2e/workers/harness/iii.worker.yaml
  • code-runner/tests/e2e/workers/harness/package.json
  • code-runner/tests/e2e/workers/harness/src/cases-errors.ts
  • code-runner/tests/e2e/workers/harness/src/cases-keep.ts
  • code-runner/tests/e2e/workers/harness/src/cases-register.ts
  • code-runner/tests/e2e/workers/harness/src/cases-run.ts
  • code-runner/tests/e2e/workers/harness/src/cases.ts
  • code-runner/tests/e2e/workers/harness/src/groups.ts
  • code-runner/tests/e2e/workers/harness/src/runner.ts
  • code-runner/tests/e2e/workers/harness/src/worker.ts
  • code-runner/tests/e2e/workers/harness/tsconfig.json
  • code-runner/tests/golden/schemas/code-runner.inject-guidance.json
  • code-runner/tests/golden/schemas/code-runner.register_function.json
  • code-runner/tests/golden/schemas/code-runner.run.json
  • code-runner/tests/golden/schemas/code-runner.teardown.json
  • code-runner/tests/manifest.rs
  • code-runner/tests/schemas.rs
  • code-runner/tests/support/mod.rs
  • code-runner/ui/build.mjs
  • code-runner/ui/package.json
  • code-runner/ui/page.tsx
  • code-runner/ui/src/configuration/index.tsx
  • code-runner/ui/src/function-trigger-message/index.tsx
  • code-runner/ui/src/function-trigger-message/register-function.tsx
  • code-runner/ui/src/function-trigger-message/result.test.tsx
  • code-runner/ui/src/function-trigger-message/run.tsx
  • code-runner/ui/src/function-trigger-message/teardown.tsx
  • code-runner/ui/src/lib/shared.tsx
  • code-runner/ui/styles.css
  • code-runner/ui/tsconfig.json
  • crates/node-core/Cargo.toml
  • crates/node-core/src/allocator.rs
  • crates/node-core/src/config.rs
  • crates/node-core/src/engine.rs
  • crates/node-core/src/error.rs
  • crates/node-core/src/ids.rs
  • crates/node-core/src/lib.rs
  • crates/node-core/src/manager.rs
  • crates/node-core/src/ops.rs
  • crates/node-core/src/prelude.js
  • crates/node-core/src/protocol.rs
  • crates/node-core/src/runtime.rs
  • crates/node-core/src/wire/mod.rs
  • crates/node-core/src/wire/register.rs
  • crates/node-core/src/wire/run.rs
  • crates/node-core/src/wire/teardown.rs
  • crates/node-core/tests/golden/iii-surface.txt
  • crates/python-core/Cargo.toml
  • crates/python-core/build.rs
  • crates/python-core/src/artifact.rs
  • crates/python-core/src/config.rs
  • crates/python-core/src/error.rs
  • crates/python-core/src/lib.rs
  • crates/python-core/src/manager.rs
  • crates/python-core/src/runner.rs
  • crates/python-core/src/wrapper.py
  • crates/python-core/tests/artifact.rs
  • crates/python-core/tests/manager.rs
  • crates/python-core/tests/persistence.rs
  • crates/python-core/tests/runner.rs
  • crates/python-core/tests/wrapper.rs
  • iii-permissions.yaml
  • pnpm-workspace.yaml

Comment thread code-runner/src/config.rs
Comment thread code-runner/src/main.rs
Comment thread code-runner/src/manager.rs
Comment thread code-runner/src/manifest.rs Outdated
Comment thread code-runner/tests/e2e/run-tests.sh Outdated
Comment thread code-runner/ui/src/configuration/index.tsx
Comment thread code-runner/ui/src/lib/shared.tsx
Comment thread crates/node-core/src/wire/run.rs Outdated
Comment thread crates/python-core/src/config.rs
Comment thread crates/python-core/src/wrapper.py
Registration + reload correctness:
- py namespaces track in-flight registrations: a failing registration no
  longer destroys the interpreter a concurrent sibling is still defining
  on (deterministic regression test, mutation-verified), and a register
  racing a teardown errors cleanly instead of panicking on the map entry
- py_handler loads default_timeout_ms per invocation so registered
  handlers honour hot config swaps, per the Manager struct contract
- one post-registration refresh closes the boot window where
  configuration:updated could fire before the trigger existed; refreshes
  serialize fetch->store so an older get cannot overwrite a newer snapshot
- manifest default_config is serialized from the struct (the hand list
  had drifted by four keys); its test now compares keysets to the schema

Core crates:
- python clamp_timeout/clamp_memory floor a zero ceiling instead of
  panicking (u64::clamp with min > max); Semaphore::new(0) hang guarded
- wrapper.py: allow_nan=False so NaN/inf results become null instead of
  poisoning the envelope (bridge frames too); the traceback cap moved
  into write_error; malformed iii.json degrades to no-bridge
- cache_root falls back to the temp dir when HOME/XDG_CACHE_HOME are
  unset instead of panicking on the boot path
- capture_streams counts the appended newline against MAX_LOG_BYTES
- run_with inlined into run (sole caller, work_dir always None)
- wire docs: node-engine::<runtime_id>:: / node-engine::teardown ->
  code-runner names (goldens regenerated)

Docs/catalog contract:
- run.code and register.function_id descriptions state the real model:
  async fn body vs module, iii.files vs /work, and one runtime per
  (namespace, lang) — the language gate they described was rolled back
- scratch_* documented as node-only (python /work is a fixed engine
  budget) in the schema docs, README, and config form

Console UI:
- CSS.escape on the focusField deep-link before querySelector, so a
  hostile fragment cannot unmount the form
- errorInfo keeps message total when error is undefined
- configForms remover joins the teardown list; copyText removes its
  textarea in finally; pulse honours prefers-reduced-motion
- build.rs watches packages/console-ui so edits there rebuild page.js

Tests/tooling:
- run-tests.sh: --filter without a value exits 2 (was an infinite loop);
  a missing HARNESS_DONE reaches its diagnostic under set -e
- stream-cap e2e asserts the real 16 KiB max_stream_bytes default (was
  <=2 MiB); the register e2e 'any' assertion is de-tautologized; the
  truncate UTF-8 test asserts the observable contract; the react import
  assertion matches the full specifier

Verified: fmt/clippy clean; node-core 227, python-core 76, code-runner
90 tests; UI tsc + vitest 11/11; e2e suite 25/25.
@andersonleal
andersonleal merged commit 5e8e466 into main Aug 12, 2026
17 checks passed
@andersonleal
andersonleal deleted the feat/code-runner-worker branch August 12, 2026 18:42
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