Plugin collection for Prax. Each subfolder is a self-contained plugin with its own plugin.py.
| Plugin | Version | Description |
|---|---|---|
txt2presentation |
1 | Any text source → narrated video presentation (Beamer + TTS + ffmpeg) |
elevenmusic |
1 | Generate songs with ElevenLabs Music API |
radio |
1 | Stream audio files as an internet radio station |
imagegen |
1 | Generate and edit images with the OpenAI Images API (gpt-image-1) |
Tell Prax:
"Import the txt2presentation plugin: https://github.com/praxagent/prax-plugins"
Or by URL with path:
"Import this plugin: https://github.com/praxagent/prax-plugins/tree/main/txt2presentation"
Prax clones the repo as a git submodule and loads only the plugin you specified.
"Import all plugins from https://github.com/praxagent/prax-plugins"
Prax clones the repo and loads every plugin subfolder that has a plugin.py.
cd /path/to/prax/workspaces/<your-user-id>/plugins/shared/
git submodule add https://github.com/praxagent/prax-plugins.git prax-pluginsEvery plugin declares its version as a string constant at the top of plugin.py:
PLUGIN_VERSION = "2"Prax reads this constant (via regex, without importing) to track which version is active, display it in the UI and catalog, and decide when to back up the previous version for rollback.
Bump PLUGIN_VERSION in every commit that changes user-facing behavior:
| Change | Action |
|---|---|
| New tool added or removed | Bump version |
| Tool arguments or return format changed | Bump version |
| Bug fix that changes output | Bump version |
| Internal refactor, no behavior change | Optional — bump if you want it visible |
| Docs-only change (Skills.md, comments) | Don't bump |
The version is a free-form string. Use whatever scheme you prefer — "2", "2.1", "2024.03". Prax compares versions as opaque strings (old != new = version changed), it does not interpret semver.
When Prax loads a plugin whose PLUGIN_VERSION differs from what the registry recorded:
- Backs up the previous
plugin.pyasplugin.py.prev - Records the new version in
registry.json(active_version), saving the old one asprevious_version - Resets the failure counter to 0
- Regenerates the plugin catalog
- Rebuilds the agent tool graph so the LLM sees the updated tools immediately
If a plugin tool fails 3 times consecutively, Prax automatically rolls back:
- Restores
plugin.py.prevover the currentplugin.py - Swaps
active_versionandprevious_versionin the registry - Sets status to
rolled_back - Reloads the tool graph
You can also trigger a manual rollback:
"Roll back the txt2presentation plugin"
Once installed, ask Prax to pull the latest version:
"Prax, please update the prax-plugins plugin"
or more specifically:
"Update the txt2presentation plugin"
Or use the Plugins panel in the TeamWork settings UI — click the refresh icon on any plugin.
Prax runs plugin_import_update("prax-plugins") under the hood, which:
- Pulls the latest commit via
git submodule update --remote --merge - Compares the old and new git commit hashes — if identical, returns
"up_to_date"and stops - Re-scans the updated code for security warnings (AST + regex)
- If clean — hot-reloads the plugin tools immediately, no restart needed. The new
PLUGIN_VERSION(if changed) is picked up automatically and recorded in the registry - If warnings are found — the plugin stays deactivated until you explicitly acknowledge the warnings (via chat or the UI)
- Commits the submodule pointer update to the workspace git repo
"What version of the txt2presentation plugin am I running?"
Prax calls plugin_status("prax-plugins") and shows the active version, previous version, health status, and consecutive failure count.
If you prefer manual control:
cd /path/to/prax/workspaces/<your-user-id>/plugins/shared/prax-plugins/
git pull origin mainThen tell Prax to reload:
"Reload plugins"
When you import a plugin repo, Prax:
- Clones the repo as a git submodule into your workspace at
plugins/shared/<repo-name>/ - Scans all Python files for security risks using both AST analysis and regex pattern matching (subprocess, eval, os.environ, socket, etc.)
- If warnings are found — Prax shows them to you, emits a
plugin_security_warnaudit event, and waits for explicit confirmation before activating - If clean — the plugin tools are loaded immediately
- Tags the plugin with trust tier
importedand emits aplugin_importaudit event
All plugin lifecycle events (import, activate, block, rollback, remove, security warnings) are recorded in the workspace trace log and searchable via search_trace.
Prax tags every plugin with a trust tier based on its origin:
| Tier | Meaning |
|---|---|
builtin |
Ships with Prax |
workspace |
User-created in their workspace |
imported |
Cloned from an external repo (like this one) |
Imported plugins default to the least-trusted tier. Trust tiers are visible in plugin_list and plugin_status.
When you import a specific subfolder from a multi-plugin repo, Prax writes a filter file (.reponame_plugin_filter) next to the submodule so only that subfolder's plugin.py is activated. The filter lives outside the submodule to avoid modifying its git working tree.
Prax monitors every plugin tool invocation. If a tool fails 3 times consecutively, the plugin is automatically rolled back to its previous version. You'll see a message like:
"Plugin txt2presentation auto-rolled back after 3 consecutive failures."
You can check health status with plugin_status and manually roll back with plugin_rollback if needed.
Any text source → Beamer LaTeX + speaker notes (LLM) → slide images → TTS audio → video (ffmpeg)
Accepts: PDF files, web pages, YouTube videos, audio files (MP3/WAV), plain text, Markdown.
| Tool | Description |
|---|---|
text_to_presentation |
Full pipeline: any source → narrated video (.mp4) |
text_to_slides |
Lighter: any source → Beamer slide deck + speaker notes (no video) |
| Source | How it's handled |
|---|---|
| PDF URL or file | Downloaded, text extracted via pymupdf or pdftotext |
| Web page URL | Fetched, HTML stripped to plain text |
| YouTube URL | Transcript via yt-dlp subtitles, or audio download + Whisper |
| Audio file (MP3, WAV, M4A) | Transcribed via OpenAI Whisper API |
| Text/Markdown file | Read directly |
| Raw text (>200 chars) | Used as-is |
System dependencies (sandbox container):
apt install texlive-latex-base texlive-latex-recommended \
texlive-fonts-recommended poppler-utils ffmpeg yt-dlpAPI keys — handled by the framework via the capabilities gateway.
The plugin never sees raw API keys. Ensure OPENAI_KEY (or ELEVENLABS_API_KEY)
is set in Prax's .env.
Optional TTS configuration (in Prax settings):
| Config key | Values | Default |
|---|---|---|
presentation_tts_provider |
openai, elevenlabs |
openai |
presentation_tts_voice |
Any voice name for the provider | nova (OpenAI), Rachel (ElevenLabs) |
"Turn this article into a presentation: https://example.com/article"
"Make a video from this YouTube video: https://youtube.com/watch?v=..."
"Create slides from paper.pdf — business style, no video"
"Turn this podcast into slides: episode.mp3"
- Detects the source type and extracts text (PDF, HTML, YouTube transcript, audio transcription, or plain text)
- Generates Beamer LaTeX slides via your configured LLM, with natural speaker notes
- Compiles the LaTeX to a PDF slide deck
- Converts each slide to an image (pdftoppm, 300 DPI)
- Narrates each slide with TTS (OpenAI or ElevenLabs)
- Assembles each slide image + audio into a video segment (ffmpeg)
- Concatenates all segments into a final .mp4
- Saves the video, LaTeX source, and speaker notes to your workspace
| File | Description |
|---|---|
<title>.mp4 |
Narrated video presentation |
<title>_slides.tex |
Beamer LaTeX source |
<title>_slides.pdf |
Compiled slide deck |
<title>_notes.md |
Speaker notes (markdown) |
Any source (URL, file, text)
│
├─ YouTube URL → yt-dlp subtitles / Whisper
├─ Web page URL → HTTP fetch + HTML strip
├─ PDF URL/file → pymupdf / pdftotext
├─ Audio file → Whisper transcription
├─ Text file → read directly
│
▼
Extracted text
│
├─ LLM (GPT-4o / Claude / etc.)
▼
┌──────────────┐ ┌───────────────────┐
│ Beamer LaTeX │ │ Speaker notes │
│ (.tex) │ │ (JSON per slide) │
└──────┬───────┘ └────────┬──────────┘
│ │
pdflatex │
│ │
▼ ▼
Slide PDF TTS API (OpenAI/EL)
│ │
pdftoppm │
│ │
▼ ▼
Slide PNGs Audio MP3s
│ │
└───────┬────────────┘
│
ffmpeg (per slide: image + audio → video)
│
ffmpeg (concat all slide videos)
│
▼
Final .mp4 presentation
Generate songs with the ElevenLabs Music API and save them as MP3 to your workspace.
| Tool | Description |
|---|---|
generate_song |
Generate a song from a text prompt (MP3) |
API key (in Prax's .env):
ELEVENLABS_API_KEY=your_keyThis plugin declares ELEVENLABS_API_KEY under ## secrets in its permissions.md and reads it via caps.get_approved_secret(). See Plugin permissions — including the known gap: Prax currently has no way to approve that secret for an IMPORTED plugin, so the tool fails with PermissionError when this plugin is imported from this repo.
"Generate a lo-fi hip hop beat for studying"
"Make a 2 minute jazz instrumental"
"Create a punk rock song about debugging at 3am"
Parameters: prompt (required), duration_seconds (3–600, default 30), instrumental (default false).
Stream a directory of audio files as an internet radio station. All listeners hear the same broadcast in real time.
| Tool | Description |
|---|---|
start_radio |
Start streaming from a directory of audio files |
stop_radio |
Stop the station and disconnect listeners |
radio_status |
Check what's playing, listener count, and URL |
radio_skip |
Skip to the next track |
radio_queue |
Show upcoming tracks |
No API keys — just audio files in a directory. Supports MP3, OGG, WAV, FLAC, AAC, M4A.
Optional: install ngrok for public access (expose_ngrok=True).
Known gap (2026-09): expose_ngrok=True does not work with the shipped radio/permissions.md. The plugin launches ngrok with caps.run_command(["sh", "-c", "nohup ngrok http …"]) and stops it with pkill, but ## allowed_commands lists only ngrok, ffprobe, which; Prax rejects any run_command whose argv[0] is not listed (prax/plugins/capabilities.py, run_command), the plugin swallows that error, and the tool reports "ngrok not available" even with ngrok installed.
The station itself is a stdlib http.server.HTTPServer bound to 0.0.0.0 (all interfaces) with no authentication — anyone who can reach the port can listen and read /status and /playlist. See radio/README.md.
"Start a radio station from my music folder"
"Start Prax Radio with shuffle on and expose it via ngrok"
"What's playing on the radio?"
"Skip this track"
Listeners connect with any media player: vlc http://localhost:PORT/stream
| Endpoint | Returns |
|---|---|
/stream |
Audio stream (SHOUTcast-compatible) |
/status |
JSON: current track, listeners, uptime |
/playlist |
JSON: full playlist with position |
Every Prax plugin needs a plugin.py with a register(caps) function that
receives a PluginCapabilities instance:
PLUGIN_VERSION = "1"
PLUGIN_DESCRIPTION = "What this plugin does"
from langchain_core.tools import tool
_caps = None
@tool
def my_tool(arg: str) -> str:
"""Description shown to the LLM agent."""
# Use caps for all credentialed operations:
# _caps.build_llm() — get an LLM (plugin never sees API key)
# _caps.http_get(url) — audited HTTP request
# _caps.run_command([...]) — run a shell command (sandbox)
# _caps.save_file(name, b) — save to workspace
# _caps.get_config(key) — read non-secret config
# _caps.tts_synthesize(...) — text-to-speech
# _caps.transcribe_audio(path) — speech-to-text (Whisper)
# _caps.shared_tempdir() — create a temp directory
return "result"
def register(caps):
"""Return the tools this plugin provides.
Receives a PluginCapabilities instance for credentialed operations.
"""
global _caps
_caps = caps
return [my_tool]Every plugin must have a permissions.md declaring exactly what it can do. The framework enforces it as written for what goes through caps.*: the LLM, HTTP, command, TTS and transcription gateway methods each check that their capability is listed under ## capabilities (the filesystem entry is recognised but the file methods do not check it today), caps.run_command() rejects any argv[0] not under ## allowed_commands (when that section is present), and ## secrets is what the loader records as the plugin's declared secrets (prax/plugins/loader.py, prax/plugins/capabilities.py).
Two honest qualifications. First, the file is self-declared by the plugin author — Prax enforces whatever it says, but there is no operator review or approval step for its contents (the acknowledgement step at import time covers the code scan's warnings, not permissions.md). Reviewing the file yourself before importing is the control. Second, it governs only calls made through caps.*; plain Python that bypasses the gateway (stdlib sockets, open()) is covered only by the import-time scan and the plugin-host subprocess boundary (stripped environment, prax/plugins/bridge.py _SAFE_ENV) described under Security restrictions — no in-process runtime guard is installed today — not by this file.
# Permissions
## capabilities
- llm
- http
- commands
## secrets
- MY_API_KEY: Why the plugin needs this key
## allowed_commands
- ffmpeg
- whichSections:
| Section | Purpose |
|---|---|
## capabilities |
Which gateway methods the plugin may call: llm, http, commands, tts, transcription, filesystem |
## secrets |
Environment variable names the plugin needs, with a reason |
## allowed_commands |
Exact command names (argv[0]) the plugin may run. If present, anything not listed is blocked |
Why this matters:
- Reviewers can audit a plugin by reading one file — no need to trace Python code
- Bad actors can't sneak in new permissions — any change to
permissions.mdis visible in diffs - IMPORTED plugins without a
permissions.mdget zero capabilities
You can either contribute to this repo or create a standalone plugin repo. Standalone repos work exactly the same way — just put plugin.py and permissions.md at the root.
Tell Prax: "Import this plugin: https://github.com/you/my-plugin"
permissions.md— required for IMPORTED plugins, declares capabilities, secrets, and allowed commandsPLUGIN_VERSION— string, bump on every user-facing change (see Versioning plugins)PLUGIN_DESCRIPTION— one-line summary for the catalogregister(caps)— receives aPluginCapabilitiesinstance, returns a list of@tooldecorated functions- Use
caps.*methods — never importos.environ,prax.settings, or API keys directly - Deferred imports — import heavy dependencies inside your tool functions, not at module level
- Error messages — return user-friendly strings, don't raise exceptions from tools
- System deps — check for them at runtime via
caps.run_command(["which", ...])and return install instructions if missing
The PluginCapabilities object (caps) is the official SDK for plugins to access Prax services. Plugins never read environment variables or prax.settings directly. For the LLM, TTS and transcription paths the gateway injects the credential and the plugin never sees it. A plugin that calls a third-party REST API itself (elevenmusic, imagegen) receives the key's value from caps.get_approved_secret() and puts it in its own request headers — so in that case the key does pass through plugin code.
| Method | Description |
|---|---|
caps.build_llm(tier="medium") |
Get a LangChain LLM — plugin never sees API key |
caps.http_get(url, **kw) |
Audited, rate-limited HTTP GET |
caps.http_post(url, **kw) |
Audited, rate-limited HTTP POST |
caps.run_command(cmd, timeout=30) |
Run a shell command (audited, time-limited) |
caps.save_file(filename, content) |
Save bytes to the plugin's workspace directory |
caps.read_file(filename) |
Read a text file from the plugin's workspace directory |
caps.workspace_path(*parts) |
Get an absolute path within the plugin's scoped directory |
caps.get_config(key) |
Read a non-secret setting (blocks keys matching key, secret, token, password, credential) |
caps.get_approved_secret(env_key) |
Read a pre-approved secret by env var name (see Plugin permissions) |
caps.tts_synthesize(text, path, voice, provider) |
Text-to-speech — framework injects API key |
caps.transcribe_audio(audio_path) |
Audio transcription via OpenAI Whisper — framework injects API key |
caps.shared_tempdir(prefix) |
Create a temporary directory |
caps.get_user_id() |
Get the current user's ID |
Plugin-owned credentials (legacy): If your plugin needs its own API credentials and you want to use get_config(), use config key names that don't match the secret patterns. For example, use myservice_id / myservice_auth instead of myservice_api_key / myservice_api_secret.
Plugin permissions: For secrets that match the blocked patterns (e.g., ELEVENLABS_API_KEY), declare them under ## secrets in permissions.md and access them via caps.get_approved_secret(). See Plugin permissions below, including its known gap for IMPORTED plugins.
A plugin declares the secrets (API keys, tokens) it needs under ## secrets in permissions.md, one env var name per line with a reason:
## secrets
- ELEVENLABS_API_KEY: Authenticate with the ElevenLabs API to generate musicFor IMPORTED plugins (everything installed from a repo like this one) this is the only declaration Prax reads: the loader records the permissions.md secrets in the plugin registry as the plugin's declared permissions (prax/plugins/loader.py, _load_imported_via_bridge).
PLUGIN_PERMISSIONS (a module-level list of {"key", "reason"} dicts in plugin.py) is a legacy constant that the loader reads only for BUILTIN and WORKSPACE plugins, which load in-process; it is ignored for IMPORTED plugins. The plugins in this repo that need a key declare both.
Access at runtime is gated by trust tier:
| Tier | Behavior |
|---|---|
builtin |
Always allowed |
workspace |
Declared secrets are auto-approved at load time |
imported |
caps.get_approved_secret() succeeds only if the key is in the plugin's approved_permissions in the registry |
To read a secret at runtime:
api_key = caps.get_approved_secret("ELEVENLABS_API_KEY")The secret value is read from prax.settings using the Pydantic field alias mapping (e.g., ELEVENLABS_API_KEY → settings.elevenlabs_api_key). The raw value is never stored in the registry — only the approval flag is persisted. Unapproved access raises PermissionError with a message telling the user to approve it in plugin settings.
Known gap (2026-09): nothing in Prax approves a secret for an IMPORTED plugin. PluginRegistry.approve_permission is called only on the BUILTIN/WORKSPACE load path (prax/plugins/loader.py); there is no agent tool, HTTP route, or TeamWork UI action that calls it, and the "approve it in plugin settings" message points at a setting that does not exist. Today an IMPORTED plugin's get_approved_secret() raises PermissionError unless approved_permissions is added by hand to the registry file (prax/plugins/registry.json). This affects elevenmusic and imagegen when imported from this repo.
Prax applies multiple security layers when importing plugins. Rows that the import-time code scan flags block your plugin from activating until the user acknowledges the warnings; the last two rows (built-in tool name collisions, sandbox test) are hard enforcement that acknowledgement does not clear — a tool whose name collides with a built-in is dropped at load time, and a failed sandbox test rejects the write or activation (plugin_write, plugin_activate in prax/agent/plugin_tools.py):
| Restriction | Details |
|---|---|
No subprocess, os.system, os.popen |
Detected by AST analysis. Use caps.run_command() instead. |
No eval, exec, compile, __import__ |
Dynamic code execution is blocked. |
No os.environ access |
Plugins cannot read environment variables. Use caps.get_config(). |
No raw socket usage |
The import-time scan (regex + AST) flags socket as a warning to acknowledge; the stdlib http.server module is not flagged. Known gap (2026-09): prax/plugins/sandbox_guard.py defines an audit hook that would log (not block) socket events, listed under _MONITORED_EVENTS, but nothing in Prax calls install_all_guards() / install_audit_hook() at startup, so the guard is never installed in the Prax process, and IMPORTED plugin tools run in the plugin-host subprocess (prax/plugins/host.py), which does not import it either. No runtime socket guard is active today; the only controls on raw socket use are the import-time scan (an acknowledgeable warning) and the fact that the code runs in the plugin-host subprocess with a stripped environment (prax/plugins/bridge.py, _SAFE_ENV), which withholds secrets but does not stop the plugin from opening sockets. The contract is still: outbound HTTP goes through caps.http_get() / caps.http_post(). The shipped radio plugin binds its own http.server.HTTPServer on 0.0.0.0 (see its README). |
No direct prax.settings import |
Use caps.get_config() for non-secret values. |
| No built-in tool name collisions | Your tools cannot share names with Prax's ~100+ built-in tools. |
| Sandbox test must pass | Before activation, your plugin is imported in an isolated subprocess with a stripped environment (no API keys) and a 30-second timeout. |
If security warnings are found, Prax shows them to the user and requires explicit confirmation before activating.
Filesystem scoping: IMPORTED plugins are confined to plugin_data/{plugin_name}/ within the user's workspace. caps.save_file(), caps.read_file(), and caps.workspace_path() are automatically scoped. Path traversal attempts (e.g., ../) are blocked.
Risk classification: Plugin tools are automatically classified as HIGH risk for IMPORTED plugins (require user confirmation). BUILTIN and WORKSPACE plugin tools default to MEDIUM risk.
uv sync --extra devuv run pytest tests/ -x -quv run ruff check .Pull requests run lint + tests automatically via GitHub Actions. Merges to main trigger release-please for automated semantic versioning.
Apache 2.0