Rconsole docs · v1.4.2 GitHub

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).

Edit this page on GitHub