Skip to content

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
· 12 commits to main since this release
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.