Python SDK Beta
The Python SDK covers Rune's complete extension API and adapts the Python TUI ecosystem (Rich and Textual) for serving UIs into Rune windows. Install it with:
pip install rune-sdk
Requires Python 3.11 or newer. The TUI adapters are extras:
pip install "rune-sdk[rich]" # RichAdapter
pip install "rune-sdk[textual]" # TextualAdapter
A basic extension
The example below is the wiring half of the SDK's
examples/snippets,
a complete extension that adds a snippets command for storing and reusing
text. main.py does only metadata and setup; the logic lives in a
Snippets handler typed against SDK clients so it stays testable.
# main.py
import logging
import sys
from examples.snippets.handler import extend
from rune_sdk.extension import Metadata, Permission, serve_workspace_extension
META = Metadata(
developer_id="rune-sdk-examples",
developer_email="your@email.com",
developer_key="1234",
extension_id="snippets",
extension_name="Snippets",
extension_version="0.1.0",
permissions=frozenset(
{
Permission.STORAGE,
Permission.EDITOR,
Permission.COMMANDS,
Permission.FILE_SYSTEM,
Permission.BROWSER_RESOURCE_OPENER,
Permission.BROWSER_WINDOW_MANAGER,
Permission.NOTIFICATIONS,
}
),
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
serve_workspace_extension(extend, META)
# handler.py (the setup body: wire and register, then return)
import asyncio
from typing import Any
from rune_sdk.api import text
from rune_sdk.extension import Workspace
async def extend(w: Workspace, _config: dict[str, Any]) -> None:
s = Snippets(w) # holds w.storage(), w.editor(), w.file_system(), etc.
# React to editor events in a background task.
sub = await w.editor().subscribe_events(
text.EventType.SELECTION,
text.EventType.FLUSH,
text.EventType.CLOSE,
)
asyncio.create_task(s.handle_events(sub))
manual = text.CommandManual(
name="snippets",
summary="Insert, edit, copy, or delete reusable text snippets.",
synopsis="[insert|edit|copy|delete] <name>",
)
await w.register_command(manual, s.handle_command, s.complete)
serve_workspace_extension writes the metadata to Rune, reads back the
connection config, and serves until shutdown. The Workspace accessors
return typed clients into the host that share one channel:
| Package | Capability | Accessor |
|---|---|---|
rune_sdk.extension | Handshake, Workspace, command registration | (none) |
api.text | Editor and text operations, editor events, commands | editor(), commands(), register_command() |
api.storage | Document storage and persistence | storage() |
api.browser | Windows, tabs, resource opening, notifications, interrupts | window_manager(), resource_opener(), notifications(), interrupter() |
api.workspace | URI resolution, file operations, watchers, processes, ptys | file_system(), executor(), terminal() |
api.semantic | Language-server operations (definition, references, rename, diagnostics, and more) | lsp() |
api.llm | LLM access (models, token counting, messages) | llm() |
api.debug | Debug-adapter (DAP) control | debugger() |
api.syntax | Tree-sitter structural search and queries | parser() |
api.config | Workspace configuration | config() |
Every accessor is gated on the matching Permission declared in Metadata.
Declare dependencies in pyproject.toml
A Python extension is a regular Python project. Rune runs a .py
entrypoint with uv run, using the uv that the python package
provides and the nearest pyproject.toml above the script as the
project. uv builds the environment it declares on first launch:
# pyproject.toml
[project]
name = "snippets"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["rune-sdk"]
Depend on an extra (rune-sdk[rich], rune-sdk[textual]) when your
extension uses the TUI adapters. Rune pins uv to the extension's own
project, so the environment comes from this pyproject.toml, never from
whatever project the workspace happens to be in.
Run and debug it
Start your script in a workspace from the console:
extensions start snippets /path/to/snippets/main.py --config '{"key":"value"}'
Rune runs the .py entrypoint through uv as described above (see
source-file extensions), so there
is no build step: edit the script and extensions restart snippets.
Use extensions logs snippets to read what the process wrote to stderr;
configure logging to write there, as in the example above. The full
lifecycle and the permission prompts are covered in
the development loop.
Test it
Keep main.py to metadata and setup and put the logic in a handler typed
against SDK clients, as in the example above, so it can be exercised with
pytest in table-driven tests. Each API package ships a testing module
with fake-service harnesses, UI handlers are tested with the
rune_sdk.tui.testing harness (draw_handler, run_handler_sequence), and
the SDK's own
tests/e2e
shows extensions tested end to end against in-process fake hosts.
Package it
A Python extension is distributed from a public git repository: no
archive to build, no launcher, no bundled interpreter, one repository for
every platform. Put the project (its pyproject.toml and sources) at the
repo root next to a config.yaml that requires the python package and
points path at the entry script:
requirements:
- python
extensions:
snippets:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.py'
config:
# defaults surfaced in the user's config, editable like any other setting
Push it to any host Rune can clone over HTTPS (GitHub, GitLab, Bitbucket, or
a self-hosted server) and users install it by its <host>/<owner>/<repo>
ID:
pkg install github.com/<owner>/<repo>
Rune clones the repo, installs the python package (which provides uv)
first because the overlay requires it, and runs main.py with uv run
against the checked-out pyproject.toml, which declares the extension's pip
dependencies. Tag a commit to give users a pinnable version. See the
packages guide for the complete
walkthrough, including how tags and commits select a version.