Pythonator

Architecture

Pythonator is about 8,700 lines of typed Python in three kinds of process. This page shows what runs where, and why.

Processes

 ┌──────────── the window (PyQt6) ────────────┐
 │ service list · console · editor · dialogs   │   only shows things and sends requests
 └──────────────────────┬──────────────────────┘
                        │ local socket, token, JSON lines
 ┌──────────────────────┴──────────────────────┐
 │ background service (pythonator --daemon)    │   keeps running when the window closes
 │ supervisor · restarts · schedules · logs    │
 │ plugin host · stats                         │
 └───────┬──────────────┬──────────────┬───────┘
         │              │              │  stdin/stdout pipes
   ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴──────┐
   │ sandbox   │  │ container │  │ plugin     │   each in the OS sandbox
   │ launcher  │  │ engine    │  │ worker     │   (or a container)
   │  └ service│  │  └ service│  │  └ plugin  │
   └───────────┘  └───────────┘  └────────────┘

The window is a PyQt6 application. It holds no state of its own beyond what the background service tells it, and it never starts a service itself. Closing it, or it crashing, leaves the services alone.

The background service is a Qt event loop without widgets (QCoreApplication). It owns the services, their logs and the plugins. The window starts it when it isn’t running.

Each service starts through the sandbox launcher, the container engine, or directly (Full access). Each plugin runs as pythonator --plugin-worker in the same kind of sandbox.

Inside the background service

One thread runs the event loop and owns all state. Other threads only read pipes and hand what they read over:

Thread Does
Main Handles requests, decides restarts and schedules, writes logs, sends events. Ticks every 50 ms
One reader per service Reads the program’s output in blocks and puts it on a queue
One per plugin, two pipes Reads the plugin’s messages (and its stderr) and puts them on the queue
Stats Every 2 seconds, CPU and memory of every process tree; every 6 seconds, their ports

The main thread drains the queue for at most 30 ms a tick and sends output in batches, so a program printing a million lines a second can’t starve the rest. Nothing waits for a plugin: messages to plugins are written to pipes, and a policy check has a 2-second deadline.

The sandbox launcher

launcher.py is the one file that runs inside the sandbox, before the service. It gets a plan as JSON (folders, ports, network on or off), applies it to itself, and then execs the service, which inherits every restriction and can’t remove them.

  • Linux: applies Landlock (files, TCP ports, scoping), installs a seccomp filter through libseccomp, and runs under bubblewrap’s namespaces when bubblewrap works.
  • Windows: creates the AppContainer profile, grants the folders, and starts the program in it, suspended, inside a job object, then resumes it.
  • macOS: the sandbox is a Seatbelt profile passed to sandbox-exec, so the launcher isn’t needed.

It uses only the standard library (ctypes for the system calls) and runs with python -I -S, so nothing on the service’s path or in its folder can change it. If any step fails it exits with code 126 or 127 and the service is reported as the sandbox couldn’t start it.

Files

File Written by Format
services.json Background service ServiceFile, pydantic, validated on load; a file with bad entries is kept as .bad
settings.json Background service Settings
running.json Background service Process id and start time of each running service, to clean up after a crash
logs/*.log Background service Rotating text logs, secrets masked
daemon.token Background service A fresh random token on each start

Every write goes to a temporary file, is flushed to disk, and replaces the old file in one step, so a power cut leaves the old version or the new one, never half of each.

The code

pythonator/
  __main__.py      entry point: window, --daemon, --plugin-worker, --sandbox-exec
  paths.py         data folders, how to start Pythonator's own helpers
  launcher.py      the sandbox launcher (standard library only)
  sdk.py           the plugin SDK (standard library only)
  core/            no Qt: models, validation, store, runtimes, git, schedules, logs, ANSI, environment checks
  isolation/       building the command for each mode: linux, macos, windows, container
  daemon/          the background service: supervisor, runner, server, protocol, client
  plugins/         manifest, trust (official hashes), host, worker
  ui/              the window
plugins/           the official plugins: guard, health, webhooks
tests/             pytest, run on Linux, macOS and Windows in CI
docs/              these pages; website/ is their design

core and isolation import no Qt, so they can be tested and reused without a display.

Libraries

Pythonator uses well-maintained libraries rather than its own versions of them, and only a few:

Library For
PyQt6 The window, the event loop of the background service, the local socket
QScintilla The editor: Scintilla’s lexers, folding and speed with big files
pydantic Every stored file, request and plugin message is a typed model, checked on the way in
psutil Process trees, CPU, memory, ports
platformdirs The right data folder on each system
cronsim Cron schedules

And the tools already on your computer: git, docker or podman, bwrap, sandbox-exec. Pythonator calls their command lines instead of reimplementing them, with fixed argument lists.

Not used on purpose: Qt’s JSON and CBOR classes (QJsonDocument, QCborValue), which had a parser vulnerability; the linter refuses them. Python’s json module parses everything.