Skip to content

Releases: hami9/runcode

runcode v1.4.0

Choose a tag to compare

@hami9 hami9 released this 29 Sep 16:20
Immutable release. Only release title and notes can be modified.
48e81e1

Project settings, encrypted secrets and fixes for editor data loss and service lifecycle bugs.

Changes

  • Configure environment variables, secret values, ports, restart policy, boot startup and resource limits from each project's settings.
  • Store secrets in the Android Keystore-backed vault. Project metadata keeps references; known secret values are masked in logs, crash reports and MCP text results.
  • Update plain settings through the new update_project_settings MCP tool. Manage secret values in the app.
  • Preserve unsaved editor content when changing projects, and keep the buffer if saving fails.
  • Fix restart retry counts, cancel pending restarts on Stop All, and keep blocked workers in the stopping state until they exit.
  • Reject archive entry points outside the source folder and remove imported secret references. Imported secrets must be configured again.
  • Use real Telegram polling and HTTP server templates for new projects. Existing scripts stay unchanged.

Install

Download runcode-v1.4.0.apk. Version code is 5, minimum Android version is 8.0 (API 26), and supported ABIs are arm64-v8a and x86_64.

The APK uses the same release certificate as v1.3.0, preserving signing compatibility for updates. Certificate SHA-256: 9e9e346c4a745f9d25c838b04e29221da3258f81b858ec3cb842449e19810568.

Validation

  • 32 JVM/Robolectric tests and five Python regression tests pass.
  • Android lint passes with no errors; dependency and resource warnings remain.
  • Release APK builds and its v2/v3 signatures verify.
  • SHA256SUMS.txt contains the checksum for the attached APK.

Known limits

  • One Python workload can run at a time because the embedded interpreter shares its environment and working directory. Static web services can still run alongside it. Python scripts and MCP clients remain trusted code with app privileges.
  • UI, Android Keystore and upgrade flows have not been tested on a device for this version. A live Telegram round trip has not been tested.
  • Exported source and data files are copied as written and may contain secrets placed there by user code; vault contents are excluded.

Changes: #1

runcode v1.3.0 — file manager, zip import/export, editor fixes

Choose a tag to compare

@hami9 hami9 released this 22 Sep 10:35
Immutable release. Only release title and notes can be modified.

Phase 2 of the roadmap: a real file manager for projects, zip import/export, and four new MCP
tools. Testing it on device also turned up a crash and several layout bugs that were already
in the app; those are fixed here too.

File manager

Editor → folder icon. It shows the whole project, not just source/, so whatever a
script writes into data/ is visible.

  • Create files and folders (utils/helpers.py creates the folder too), rename, move to
    another folder, delete — with a confirmation that says how many files a folder holds.
    source/, data/, config/, logs/, cache/ and backups/ are the project layout and
    cannot be renamed or deleted; their contents can.
  • Renaming or moving the entry point takes the project with it — rename main.py and Run
    still works. ⋮ → Set as entry point picks another file.
  • Import files here copies files in from the phone; an existing file is never
    overwritten (scores.csv → scores (1).csv). Save a copy to phone gets any file back
    out, including databases a script produced.
  • Binary files and files over 512 KB no longer open in the editor as garbage; the app says
    what they are and how to get them out.

Import and export whole projects

⋮ → Export project (.zip) writes code, data and settings. Projects → import icon
brings it back as a new project — verified byte-identical after export → delete → import.

Any other zip works too: a GitHub "Download ZIP" is unwrapped from its top folder, macOS
junk is skipped, and the entry point is detected (main.py, bot.py, app.py, … or
index.html for a website).

Imports are defensive: an archive entry that tries to write outside the project is refused
(tested with ../../../shared_prefs/…; nothing was written), expansion is capped at 512 MB,
and imported projects never start on boot until you turn that on. Exports never contain
secrets — a secret variable is written only as its ${SEC_...} reference.

MCP

Sixteen tools now. New: create_directory, rename_path (rename or move; the entry point
follows), delete_path, and set_entry_point. read_file refuses binary and oversized
files with their size instead of returning mojibake, and list_files covers the whole project
with folders marked by a trailing /.

Edits made over the bridge appear in the app straight away: the tree refreshes, and a file
open in the editor reloads unless you have unsaved changes in it.

Fixed

  • Crash when opening a file. Opening a file that added a second tab could crash the app
    with IndexOutOfBoundsException: Material3's default tab indicator can recompose with the
    new tab index before the new tab is measured. This may be the crash reported earlier.
  • Line numbers didn't scroll. The numbers stayed put while the code scrolled under them.
    The editor now keeps its own scroll position and draws each number at the line's baseline;
    long lines wrap and get one number, and the view follows the cursor while you type.
  • Unsaved edits were lost when switching or closing a tab. They are saved now.
  • "New file" with an existing name wiped that file. It now refuses.
  • A long project name pushed Run off the editor toolbar; project cards hid their
    edit / backup / delete buttons on phone-width screens; the Home New button wrapped
    to "Ne / w".
  • Every path now goes through one containment check that requires a separator, so a sibling
    directory sharing a prefix can no longer slip past it.

Install

Signed with the same key as v1.2.0 (certificate SHA-256 9e9e346c…810568), so it installs
over it. versionName is now 1.3.0 — earlier releases all still reported 1.0.0.
minSdk 26, ABIs arm64-v8a and x86_64. Licensed MIT; third-party notices in
THIRD_PARTY_LICENSES.md.

runcode v1.2.0 — Python 10x faster, no more mixed-up output

Choose a tag to compare

@hami9 hami9 released this 20 Sep 16:37
Immutable release. Only release title and notes can be modified.

Testing the runtime properly on device turned up four defects, all fixed here, plus the
first phase of resource monitoring.

Python was 25x slower than it should have been

run_script installed a sys.settrace hook so a running script could be interrupted — and
that hook fires on every line. Same loop, same process, measured on device:

WITH tracer      0.945s  ->   317,478 iter/s
WITHOUT tracer   0.037s  -> 8,134,909 iter/s

Stopping now uses PyThreadState_SetAsyncExc through ctypes: it raises in the target thread
at its next bytecode boundary and costs nothing while the script runs normally. A script
blocked in time.sleep(1) still stops 0.8 s after you press Stop.

Real-world effect: a plain arithmetic loop went from 317k to 1.2–4.6M iterations/second.

Two services stole each other's output

sys.stdout is process-global and each script replaced it, so whichever started last
captured both. With a Python project and a Telegram bot running together, A tick 11 showed
up in the bot's log. Output is routed per thread now — verified with two concurrent services,
each log containing only its own lines.

The log file was out of order

A coroutine was launched per line, so lines interleaved on the way to disk while the
in-memory view stayed correct. One consumer drains a queue instead. Twenty lines printed in
sequence now land in sequence.

A broken bridge triggered a restart loop

If the Python bridge module itself failed to load, that was classified as a crash, so
ON_FAILURE projects kept retrying something that could never work. It is fatal now.

Resource monitoring and limits

The app used to report cpuPercent = 2 for Python and 1 for the web server — both
hardcoded. Now:

  • CPU is sampled per service from /proc/self/task/<tid>/stat, differenced between
    heartbeats. Every service already runs on its own named thread, so the attribution is real.
  • Memory comes from Debug.MemoryInfo; the System screen shows PSS, Java heap against the
    device's limit, thread count and pid.
  • Projects can set CPU %, heap MB and idle minutes limits (blank = unlimited).
    A sustained breach marks the service DEGRADED, then stops it:
Stopping 'Python Quickstart': CPU 99% over the 20% limit for 9s.

This is runcode policing its own services. An Android app cannot free another app's memory or
tune the CPU — there is no API for it — and nothing here pretends otherwise.

Install

Signed with the same key as v1.0.x, so it installs over it. minSdk 26, ABIs arm64-v8a and
x86_64. Licensed MIT; third-party notices in THIRD_PARTY_LICENSES.md.

runcode v1.0.1 — real Telegram bots, no more restart flapping

Choose a tag to compare

@hami9 hami9 released this 12 Sep 15:24
Immutable release. Only release title and notes can be modified.

Everything here came out of testing v1.0.0 on a real phone.

Third-party Python packages

A real bot script used to die on from telegram import Update no matter what, because only
the standard library shipped. pip now resolves packages at build time and they are packaged
into the APK: python-telegram-bot 21.9 and requests.

Verified on device — the library loads, reaches api.telegram.org over the bundled OpenSSL,
and the server answers:

python-telegram-bot version: 21.9
server replied: InvalidToken - The token `1111...:***REDACTED***` was rejected by the server.

(That redaction is the log sanitiser doing its job on a real bot token.)

Add more packages to the pip block in app/build.gradle.kts. pip runs on the build
machine, so building now needs a CPython 3.12 — set RUNCODE_BUILD_PYTHON if it is not on
PATH.

Fixes

  • A syntax error made the service flap. The Telegram profile restarts ON_FAILURE, so
    the supervisor retried a SyntaxError four times with backoff even though it fails
    identically every time. Scripts that cannot load at all — SyntaxError,
    ModuleNotFoundError — are now reported as fatal and never retried.
  • Battery optimisation state was stale. It was read once at startup, so granting the
    exemption in Settings and coming back still showed "OPTIMIZED". Re-read on every resume.
  • Memory was printed raw — RAM: 17.93731689453125MB — which also overflowed the Home
    card and squeezed its Stop button down to a bare icon. Formatted to one decimal.
  • Crashes are now readable. Android's dialog says only "Something went wrong" and the
    trace is only reachable over adb, which is no use on a phone. An uncaught-exception
    handler persists the stack trace and the System screen shows it with a copy button.

Install

Signed with the same 4096-bit RSA key as v1.0.0 (v2 + v3 schemes), so it installs over it.
minSdk 26, ABIs arm64-v8a and x86_64.

Licensed MIT. Third-party notices: THIRD_PARTY_LICENSES.md.

runcode v1.0.0 — real CPython, working web server, shell and MCP bridge

Choose a tag to compare

@hami9 hami9 released this 12 Sep 14:09
Immutable release. Only release title and notes can be modified.

runcode v1.0.0

An on-device Android runtime: real CPython, a working local web server, a shell, and an
authenticated MCP bridge so an AI client can drive the whole thing.

Tested on Android 16 (API 36). minSdk 26.

Highlights

Real Python. CPython 3.12 is embedded via Chaquopy. Scripts run as __main__ with the
project directory as the working directory, so relative paths like data/tasks.sqlite
resolve the way they do on a desktop. Output streams into the console line by line while the
script runs, and a long-running loop can be interrupted from the UI or over MCP.

A local web server that works. Static web projects are served by a built-in HTTP/1.1
server with correct content types, SPA fallback and directory-escape protection.

A shell. Interactive /system/bin/sh inside the app sandbox, with the selected project's
directory as cwd and its environment injected. No root, no PTY, no job control — it is the
shell every Android device already ships.

MCP bridge. JSON-RPC 2.0 over POST /mcp, protocol revision 2025-06-18, twelve tools:

list_projects · get_project · list_files · read_file · write_file ·
start_service · stop_service · service_status · get_logs · run_command ·
run_python · sql_query

Point any MCP client at it:

adb forward tcp:8765 tcp:8765
curl -s -X POST http://127.0.0.1:8765/mcp \
  -H "Authorization: Bearer $RUNCODE_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Security

The bridge exposes a shell, arbitrary Python and read/write file access, so:

  • a bearer token is required on every call including on loopback, compared in constant time;
  • the token is 24 random bytes kept in an Android Keystore-backed vault and rotatable from
    the UI;
  • the listener binds 127.0.0.1 unless LAN exposure is explicitly enabled;
  • secret-valued environment variables are returned as <secret>, never echoed;
  • GET /health is the only unauthenticated route and reveals nothing about the device;
  • the process is held in the foreground while the bridge is online.

The secret vault itself now uses an Android Keystore key with a random IV per value, and
refuses to store a value it cannot encrypt. Backup rules no longer ship the vault to cloud
backup.

Backup archives are checksum-verified for real. The screen always claimed they were;
nothing was ever compared. Both Verify and Restore now refuse an archive whose payload does
not hash to the value in its manifest.

Fixes

  • A fresh clone could not build: the signing config pointed at a debug.keystore that was
    not in the repo. There is now a Gradle wrapper and a .gitignore as well.
  • Static web projects crashed the app with NoClassDefFoundError on
    com.sun.net.httpserver, which exists in the JDK but not on Android.
  • Every log line crashed the app on Android 8–13: Matcher.replaceAll(Function) is API 34+
    and was called on each line with minSdk 26. FOREGROUND_SERVICE_TYPE_SPECIAL_USE had the
    same problem.
  • A script that finished successfully was reported as FAILED.
  • A port conflict picked a free port, logged it, then bound the occupied one anyway.
  • The wake lock was re-acquired on every automatic restart and never released.
  • Environment variables, arguments and the working directory were not persisted, so a
    configured Telegram bot token vanished on restart.
  • The bottom navigation could not fit its destinations and wrapped labels mid-word.
  • On the Backups screen the Restore button was pushed off the right edge by long archive
    names and could not be tapped, and a failed verification showed no reason.

Android lint: 11 errors → 0.

Known limitations

  • pip packages are not enabled; third-party libraries need a build-time requirements list
    in the chaquopy { } block.
  • JavaScript and PHP runtimes are not implemented and are labelled as such in the UI.
  • The MCP bridge lives in the app process and does not survive the process being killed.
  • The APK is ~52 MB because CPython ships for arm64-v8a and x86_64. Trimming
    abiFilters to a single ABI roughly halves it.

Install

runcode-v1.0.0.apk is signed with a 4096-bit RSA key (APK Signature Scheme v2 + v3),
minSdk 26, ABIs arm64-v8a and x86_64. Sideload it — this is not on any store, so your
device will ask you to allow installation from an unknown source.

Licensed MIT.