rconsole — file structure
rconsole/
├── app.py # Flask app: CSRF + auth guards, login throttling, confined file APIs, proxy, /ws
├── main.py # Entry point (calls app.main())
├── config.py # All environment-driven settings + persisted secrets
├── FREE_HOSTING.md # The purpose of rconsole: free Render-tier hosting for bots/sites/workers (start here)
├── fileops.py # File-manager engine: compress / extract (safe, no symlink writes), properties, multi-delete, background jobs
├── keepalive.py # Self-ping of the public URL so free instances are not suspended for inactivity (+ cmd_keepalive.py)
├── backup.py # Encrypted snapshots -> private git repo/folder; restore on a fresh instance (+ cmd_backup.py)
├── bootstrap.py # Imported first by app.py: restores a backup BEFORE secrets are generated
├── services.py # Supervisor: named commands that autostart + restart on crash (+ cmd_service.py)
├── cmd_reboot.py # `sudo reboot`: final backup, stop everything, reload in place (or --hard exit)
├── cmd_update.py # `rconsole version` / `rconsole update`: check, confirm, start the self-update
├── updater.py # self-update engine: lock, download+verify, test, atomic swap, rollback, boot safety
├── _pathfix.py # imported first by app.py: puts dependencies installed by an update on sys.path
├── FEATURES.md # every feature, by area
├── render.paid.yaml # Render blueprint with a persistent disk (render.yaml is the FREE plan)
├── LICENSE # MIT
├── _ptyexec.py # PTY launcher: setsid + controlling terminal (job control, Ctrl+C), then exec
├── sandbox.py # THE authorization choke point: host vs per-user VM (proot), path confinement
├── distros.py # VM system catalog (alpine/debian/ubuntu/kali) + checksum-verified resolvers
├── interp.py # Command parsing/dispatch; interactive detection; host passthrough
├── commands.py # Thin registry (COMMANDS/PROTECTED/@command) + module wiring
├── cmd_basic.py # help, clear, history, rwhoami, cd, alias, theme, shell (admin host window)
├── cmd_hosting.py # kprocess (kill a process by PID)
├── cmd_users.py # logout, suser/auser/nauser/ruser, muser/cpass/cuser, rboot
├── cmd_vm.py # vm: list / install <name> / shell / run / mode / reset / remove / status
├── SECURITY.md # How to report a vulnerability (links to docs/security.md)
├── docs/ # Documentation-site sources: index, quick-start, backup, upgrading, configuration (generated table),
│ # security, architecture, compatibility. The site also renders README/FEATURES/DEPLOY/COMMANDS/... as they are.
├── scripts/envvars.py # THE list of every environment variable (name, default, description); tables in the docs are generated from it
├── scripts/build_docs.py # `--write` regenerates the tables, `--check` fails if stale, `--site DIR [--strict]` builds the GitHub Pages site
├── .github/workflows/docs.yml # builds and publishes the site to GitHub Pages
├── devsetup.py # `vm setup <profile>`: per-family package sets + install scripts (python, build, node, ...)
├── cmd_fetch.py # neofetch / fetch (target aware, width responsive) + rfetch
├── vmtools/
│ └── rfetch.sh # POSIX shim installed in every VM as neofetch / fetch / rfetch
├── cmd_jobs.py # jobs, jobout, jobtail, fg, kill
├── cmd_env.py # sudo env (per-user environment variables)
├── cmd_tooling.py # sudo opencode (external TUI launcher)
├── editor.py # In-console editor + file manager (edit, fm)
├── agent.py # Native in-console AI agent (OpenAI-compatible / Anthropic, tools)
├── fun.py # Fun/novelty commands (cowsay, fortune, figlet, neofetch, …)
├── jobs.py # Background jobs (`cmd &`, jobs/jobout/jobtail/fg/kill %n)
├── termsess.py # Persistent PTY sessions keyed by (user, tab)
├── ptyhost.py # WebSocket ⇄ terminal-session bridge (attach/detach/kill)
├── shellutil.py # Shared shell detection + subprocess/PTY environment builder
├── state.py # In-memory session/tab/host state + JSON persistence
├── auth.py # User auth & persistence (data/users.json, PBKDF2 hashes)
├── examples/ # Example hosted apps (run them with `service add … --expose`)
│ └── guestbook/app.py # Single-file Flask guestbook demo
├── gunicorn.conf.py # Production server settings (1 worker + threads: state is in-memory)
├── pidns.py # one PID namespace per VM (unshare + nsenter), `vm ps` / `vm kill` scoping
├── pidns_init.py # PID 1 of a VM's namespace: reaps zombies, exits when the server dies
├── vmdeps.py # per-VM dependency manifest (deps.json): capture before backup, reinstall after restore
├── vmupdate.py # `vm update`: lock, snapshot (hard links), upgrade, verify, rollback, crash recovery
├── dashboard.py # Admin dashboard backend: resource sampler, section views, admin actions, audit trail
├── serve.py # Launcher: gunicorn if usable, else built-in threaded server (Windows)
├── Dockerfile # Image with proot/git/tini preinstalled (Render, any Docker host)
├── docker-entrypoint.sh # Fixes root-owned volumes, then drops to uid 10001 (RCONSOLE_RUN_AS_ROOT=1 keeps root)
├── DEPLOY.md # Full deployment guide: Render, Railway, Fly, VPS, k8s, cloud, home server
├── deploy/ # docker-compose.yml, Caddyfile, nginx.conf, fly.toml, k8s.yaml, systemd unit, env examples
├── render.yaml # Render Blueprint (Docker service + persistent disk)
├── requirements.txt # Pinned runtime deps for Docker/pip (exported from uv.lock)
├── tests/ # unittest suite (run: python -m unittest discover -s tests)
│ ├── helpers.py # Isolated temp data dir; must be imported first
│ ├── test_auth.py # Hashing + migration, policy, quarantine, throttle
│ ├── test_dashboard.py # Dashboard: access control, sections, actions, audit trail, logout button
│ ├── test_serve.py # serve.py: server choice, gunicorn command, built-in fallback + shutdown
│ ├── test_sandbox.py # Path confinement (symlink/.. escapes), authorization
│ ├── test_vmupdate.py # vm update: snapshot/rollback, lock, who may update, recovery (fake package manager)
│ ├── test_vmupdate_e2e.py # vm update in a real proot VM with a fake apt-get
│ ├── test_multivm.py # Several VMs per user: separation, picker, targets API, migration, doctor
│ ├── test_devsetup.py # Every profile script is valid sh and has the headers psycopg2 & co. need
│ ├── test_distros.py # Catalog, aliases, mirror resolvers (HTTP faked)
│ ├── test_fetch.py # neofetch layouts at phone/desktop widths, target awareness
│ ├── test_app.py # HTTP: login, CSRF, file APIs, proxy, VM-backed commands
│ ├── test_fileops.py # Archives (round trips, zip-bomb / traversal / symlink-write attacks, real bsdtar / 7z / RAR), properties, jobs
│ ├── test_fileman_api.py # File-manager HTTP API: props, archive jobs, multi-delete, confined-user checks
│ ├── fixtures/ # Tiny real RAR files (from libarchive's test suite) for the extraction tests
│ ├── js/ # Node tests: editor_check.js, files_check.js (pure logic), ui_check.js (jsdom end-to-end)
│ └── test_vm_e2e.py # Real proot VM from a locally served rootfs
├── pyproject.toml # Project metadata + dependencies (uv)
├── uv.lock # Resolved dependency lockfile (uv)
├── .python-version # Pinned Python version
├── .gitignore # Ignored files (venv, caches, data, logs)
├── README.md # Project documentation
├── structure.md # This file
├── static/ # Front-end assets (served as static)
│ ├── console.js # Tab/terminal UI, xterm.js wiring, WebSocket client, editor UI
│ ├── editor-core.js # Code-editor text logic (auto-indent/close, comments, line ops, find): DOM-free, unit-tested
│ ├── files-core.js # File-manager selection / archive-name logic (click, ctrl/shift, drag band, long-press sweep): DOM-free, unit-tested
│ ├── dashboard.js # Admin dashboard UI (plain DOM + inline SVG charts, textContent only)
│ ├── dashboard.css # Admin dashboard styling (uses the console theme variables)
│ ├── style.css # Console styling + terminal layout
│ ├── logo.png, icon-192.png, icon-maskable-512.png, apple-touch-icon.png, favicon.ico, favicon-32.png # the rconsole logo in every size the app, PWA manifest and login page use
│ ├── xterm.js # xterm.js terminal emulator
│ ├── xterm.css # xterm.js stylesheet
│ └── addon-fit.js # xterm.js FitAddon (auto-fit terminal to container)
├── templates/ # Jinja2 templates
│ ├── console.html # Main console page (loads xterm.js + console.js)
│ ├── dashboard.html # Admin dashboard shell (admins only)
│ ├── login.html # Login page
│ └── error.html # Themed HTTP error page
├── data/ # Runtime data (gitignored; override with RCONSOLE_DATA_DIR)
│ ├── users.json # User accounts (hashed passwords, 0600)
│ ├── secret.key, pepper.key # Generated secrets (0600) unless set via env
│ ├── state/ # Per-user persisted tab/alias/theme/env/mode state
│ ├── logs/ # Hosted-app logs (<folder>-<port>.log)
│ ├── update/ # `rconsole update`: state.json, releases/<ver>/, previous/ (rollback), site-<ver>/ (deps)
│ └── vm/ # bin/proot, cache/ (rootfs tarballs), <user>/{rootfs,home}
└── .venv/ # Virtual environment (gitignored)
Module descriptions
Backend (Python)
config.py — Every setting, read from the environment (data dir, VM,
modes, proxy trust, throttle limits) plus persistent_secret(), which creates
random secrets once and stores them 0600 so restarts/workers agree.
sandbox.py — Execution targets and the one place permissions are decided.
- spec(sid, cmd, cwd) / argv_spec(...): argv + cwd + env for the right target
(host or vm); raises PermissionDenied for non-admins asking for the host.
@vm / @host prefixes force a target and are authorized the same way.
- fs(sid) → FsView: path namespace for file access. VM views resolve symlinks
and .. inside the root filesystem (resolve_in_root) and know the /root
bind mount, so nothing can escape.
- Several VMs per user under vm/<user>/<id>/{rootfs,home}; installed_vms,
active_vm (auto when exactly one), resolve() → Target(mode, vm) or
NeedVmChoice (the interpreter turns that into a dropdown), targets() /
set_target() for the browser menu, choice() for the inline picker,
@vm:<id> command prefix. Legacy single-VM layouts migrate on first access.
- VM lifecycle: install_many(sid, ["debian", "kali"]) (background thread: proot →
rootfs download with the distro's own checksum → safe extract → per-family
tweaks → install the rfetch shim → probe), reset, describe, distro_info,
family, rename_user. Non-admin guests get only safe /proc/* info files.
distros.py — The system catalog. parse_choice("debian:bookworm"), aliases
(deb → debian, archlinux → arch), and one resolver per system that finds the newest image and its
published SHA-256 (Alpine release index, Ubuntu SHA256SUMS, linuxcontainers
mirrors for Debian and Arch, kali.download for Kali).
updater.py — Self-update (rconsole update); stdlib only at import so the entrypoint can run it first.
- check() reads the latest GitHub release; start() / start_rollback() lock the console and run _run_update() in a
thread: download → SHA-256 against SHA256SUMS → safe_extract → check_tree → optional install_deps (into
data/update/site-<ver>) → smoke_test (import in a subprocess) → only then lifecycle.begin_shutdown() →
apply_tree (per-file atomic replace, rollback copy in data/update/previous, manifest-based removal of stale files,
never data/, .venv, .git, .env) → state → reload.
- is_locked() / status() drive app.update_gate and /api/update/status.
- Boot safety: sync() (run by docker-entrypoint.sh) re-applies the saved release over an older image and rolls a
version back after MAX_BOOTS failed starts; confirm_boot() marks a healthy one; after_restore() does the
re-apply after a backup restore on a blank disk. python updater.py sync|check|rollback|status works without the server.
cmd_update.py — the rconsole command (version, update [--check|--yes|--force|--rollback]); a generator
that asks for confirmation. _pathfix.py — first import of app.py; calls updater.activate_deps().
cmd_fetch.py — neofetch/fetch pick the VM's rfetch (vm mode) or
rconsole's own summary (host mode); layout() fits logo + details to the width
the browser reports (cols in /api/run).
app.py — The Flask application.
- origin_check() then guard() (before-request): CSRF/Origin check; login,
30-day inactivity timeout, user-still-exists, forced default-password change.
- login (throttled per ip+user and per ip), console_page, /healthz.
- /api/run: dispatches a line of input to interp.process.
- /api/media (an image or video served inline for the viewer: whitelisted types, Range support, sandboxing CSP),
/api/download (a file, or a folder as a streamed zip built with os.fwalk + O_NOFOLLOW) and
/api/upload (one file per request, rel keeps a folder upload's layout, temp file + os.replace).
- /api/file (GET/POST), /api/dir, /api/fs: editor + navigator, all mapped
through sandbox.fs (admins: whole host; others: their VM only).
- /<folder>/<int:port>/…: pooled reverse proxy to the hosted app; never
forwards the console's own session cookie; keeps multiple Set-Cookie.
- /ws: terminal websocket; the cmd query value is authorized by sandbox
before anything is persisted or spawned.
main.py — Thin entry point that imports app.main and runs it.
interp.py — Command interpreter.
- process(): routes built-ins to commands.py, interactive commands to a PTY
({"pty": True, "cmd": …}), background jobs (trailing &) to jobs.py,
and everything else to a shell passthrough in the user's target
(_passthrough → sandbox.spec, 60s timeout). The command text is passed to
the shell verbatim (pipes/redirects work); SHELL_PREFERRED lets echo/wc/…
fall through to the real tools when a line uses shell operators.
- INTERACTIVE: the set of commands run through a real PTY. Bare git is
intentionally not in the set — only the pager/editor git subcommands
(log/diff/show/status/blame/branch) take the PTY path, so a bare
git (usage help) stays in the fast captured-output path.
- Shell selection and env construction live in shellutil.py.
commands.py — Thin command registry (COMMANDS, PROTECTED, @command)
plus module wiring. No command logic lives here; each cmd_* / editor / fun
module registers itself via the decorator (same pattern agent.py uses).
interp.py dispatches via COMMANDS and enforces PROTECTED (sudo/admin-only).
cmd_basic.py — help, clear/cls, history, rwhoami, cd (VM-aware, ~), alias/unalias, theme.
cmd_vm.py — vm (public): status, install, shell ({"pty": true, "cmd": "@vm"}),
run, mode, reset, remove.
cmd_hosting.py — kprocess. (serve, gunicorn and sprocess were removed in 1.2.2:
expose and service host apps now.)
cmd_users.py — logout (public), suser / auser / nauser / ruser,
muser / cpass / cuser (interactive prompts), rboot (global factory reset).
cmd_jobs.py — jobs, jobout, jobtail, fg, kill (delegates %n to jobs.py,
anything else to the host kill).
cmd_env.py — sudo env list/set/unset/import (per-user env vars).
cmd_tooling.py — sudo opencode launcher (local binary → PATH → local
npm install opencode-ai background job).
editor.py — In-console editor (edit, nano-style line workflow) and file
manager (fm: navigate, cat/rm/mkdir/touch/mv, hand off to the browser editor).
Path helpers (_resolve_path, …) also used by fun.py's rev/wc.
agent.py — Native in-console AI agent (OpenAI-compatible / Anthropic).
- Tools: bash, read_file, write_file, list_dir (run inside rconsole's
host environment, rendered through the line console — no PTY).
- Persistent per-tab conversation, context compaction, retries, slash commands.
fun.py — Fun/novelty commands (cowsay, fortune, figlet/banner,
lolcat, sl, matrix, neofetch, colors, cal, wc, rev, date,
uptime, echo, hi/hello). Pure-Python ANSI output.
jobs.py — Background jobs (cmd &): rolling output buffer, live tail
(/api/jobtail), kill by %n or PID-tree. Env/shell via shellutil.py.
termsess.py — Persistent PTY sessions keyed by (user, tab).
- Owns the real PTY process + rolling buffer; websockets attach/detach
without killing the process; explicit kill (tab close) tears it down.
- Backends: pywinpty/ConPTY on Windows, pty/select/termios on POSIX.
ptyhost.py — WebSocket ⇄ terminal-session bridge.
- attach(): create-or-attach a termsess.TermSession, replay backlog,
pump data/resize/kill messages. run_pty() is a backwards-compatible
alias for attach().
shellutil.py — Shared helpers: detect_shell() (explicit shell
selection, never shell=True) and build_env() (venv on PATH, sane
TERM/locale/SHELL defaults, per-user sudo env vars).
state.py — Global registry (atomic 0600 state files; stores each user's mode).
- STATE: per-session tabs/state.
- ACTIVE_HOSTS: live hosted apps keyed by (folder, port) with their
Popen handles.
- register_host / kill_host / kill_all_hosts: lifecycle management.
auth.py — Authentication.
- PBKDF2-HMAC-SHA256 with per-user salt + a random persisted pepper (legacy
unsalted / old-pepper hashes are accepted once, then upgraded).
- seed_defaults() creates the ark admin (RCONSOLE_ADMIN_PASSWORD or ark).
- User file: atomic 0600 writes, mtime-cached reads, corrupt file quarantined.
- LoginThrottle: exponential lockout used by the login route.
- verify, get_user, list_users, create_user, set_password, set_username,
set_admin, delete_user, reset_to_defaults.
Front-end (static/ + templates/)
templates/console.html — Loads xterm.js, addon-fit.js, and
console.js; defines the tab bar (tabs, +, 📁 navigator, theme), terminal
container (#terms), file-navigator drawer (#files), code editor
(#editor), and footer key bar (Tab / Esc / Ctrl / arrows) for touch devices.
static/console.js — UI controller.
- Tab management; line-mode input; /api/run calls.
- openPty() / closePty() / updateView(): xterm.js lifecycle, FitAddon
auto-fit, WebSocket send/receive, sticky Ctrl for control chars.
- File navigator (toggleFiles / loadDir / renderFiles / fsApi): browse
via /api/dir, mutate via /api/fs, open files in the editor.
- Editor highlighting: langFromPath() (300+ extensions + special basenames
→ ~65 rule ids), HL_RULES token patterns, getHighlighter() compiler.
static/editor-core.js — The code editor's text logic, free of the DOM so it
can be tested in plain Node (tests/js/editor_check.js). Every function takes the
text and selection and returns one edit {from, to, insert, selStart, selEnd} (or
null to let the browser act): enter, typeChar, backspace, tab,
toggleComment, moveLines, duplicateLines, deleteLines, joinLines,
insertLine, findAll, replaceAll, detectIndent, … console.js applies the
edits through document.execCommand (so native undo/redo keeps working) from the
beforeinput / keydown handlers, and owns the status bar, font size
(--editor-fs), find bar and touch symbol bar.
static/files-core.js — The file manager's decisions without the DOM
(tests/js/files_check.js): what a click / ctrl- or shift-click / dragged rectangle /
long-press sweep selects (applyClick, bandSelection, sweepSelection), default
archive names, which formats fit a selection, auto-scroll speed. console.js owns the
elements, the pointer / touch events, the selection bar, the dialogs and the job polling.
fileops.py — Everything slow or security-sensitive behind the file manager, standard
library only: compress (zip, tar family, one-file gz/bz2/xz/zst, 7z / RAR through helper
tools), extract (native zip / tar / gz; bsdtar and friends for 7z, RAR, CAB, ISO ...),
properties, delete_paths, and a small background-job registry (start_job, get_job,
cancel_job, one running job per user). Extraction creates every member with dir_fd +
O_NOFOLLOW, so an archive can never write through a symlink; archives are never created by
following links; confined users only get bsdtar for external formats. app.py resolves paths
through the sandbox first and exposes /api/props, /api/archive, /api/archive/<id>,
/api/archive/<id>/cancel and the delete_many action of /api/fs.
dashboard.py — Backend of /dashboard (admins only, checked in app.py). A sampler thread keeps an hour of
CPU / memory / disk / network history from /proc and the cgroup; view(section) returns the Overview, Processes,
Users, Systems, Apps, Activity and Health data; perform() runs the admin actions (shared code with the sudo user
commands) and audit() records them in logs/audit.jsonl.
static/dashboard.js — The dashboard UI: tabs, tables, inline SVG charts, confirm dialogs. Everything is inserted
with textContent (a test forbids innerHTML).
static/style.css — Layout & theming, including the terminal host sizing
that makes the PTY fill the available width.
static/xterm.js, static/xterm.css, static/addon-fit.js — Vendored
xterm.js emulator and FitAddon.
Data & environment
data/ — Runtime state (see the tree above). Gitignored; do not commit.
Set RCONSOLE_DATA_DIR to put it on a persistent disk.
pyproject.toml / uv.lock — Dependencies: flask, flask-sock,
requests, and pywinpty (Windows only). Managed with uv.
.python-version — Pinned Python (>= 3.14).
