Skip to main content

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:

PackageCapabilityAccessor
rune_sdk.extensionHandshake, Workspace, command registration(none)
api.textEditor and text operations, editor events, commandseditor(), commands(), register_command()
api.storageDocument storage and persistencestorage()
api.browserWindows, tabs, resource opening, notifications, interruptswindow_manager(), resource_opener(), notifications(), interrupter()
api.workspaceURI resolution, file operations, watchers, processes, ptysfile_system(), executor(), terminal()
api.semanticLanguage-server operations (definition, references, rename, diagnostics, and more)lsp()
api.llmLLM access (models, token counting, messages)llm()
api.debugDebug-adapter (DAP) controldebugger()
api.syntaxTree-sitter structural search and queriesparser()
api.configWorkspace configurationconfig()

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:

config.yaml
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.

Ask Rune Agent