casa-clonerdocsplugins 6 min
PLUGIN SYSTEM · v1

Build & ship
Casa Cloner plugins.

Plugins are signed Python modules the desktop app loads at runtime. Ten chapters: manifest, hooks, sandbox, the publishing pipeline, and the integrity checks bound to every download.

Runtime
CPython 3.10+
Sig
HMAC-256
Cap
5 MiB
Review
Manual
plugin.pypython
def on_load(api):
    api.log("hello from %s" % api.slug)

def on_message(api, message):
    if message.content == "!ping":
        api.log("pong")

Chapters

01

Overview

Plugins are single-file Python modules that the Casa Cloner desktop app loads at runtime. Each plugin ships with a JSON manifest.json declaring its identity, requested permissions, and the hooks it implements. They're distributed through the marketplace at /plugins — only versions reviewed by an admin become installable.

01
Format
plugin.py + manifest.json
02
Runtime
CPython 3.10+, restricted builtins
03
Distribution
Server-signed, client-verified
04
Integrity
SHA-256 + HMAC + size cap
02

Quick start

STEP 01
Write two files
Create plugin.py and manifest.json on your machine.
STEP 02
Submit for review
Open Dashboard → Plugins → Create and paste both files. Pick a semver version.
STEP 03
Wait for approval
Admins read the source. Approved versions appear on the marketplace and are installable from the desktop app.

The smallest plugin that can possibly work:

pythonplugin.py
def on_load(api):
    api.log("hello from plugin '%s'" % api.slug)

def on_message(api, message):
    if message.content == "!ping":
        api.log("pong")
jsonmanifest.json
{
  "id": "hello-world",
  "name": "Hello World",
  "version": "1.0.0",
  "description": "Logs a line on load and replies to !ping.",
  "author": "your-username",
  "category": "fun",
  "permissions": [],
  "hooks": ["on_load", "on_message"]
}
03

Manifest reference

Required fields are marked. Anything not listed is ignored by the loader — custom keys are fine but won't survive review unless they're justified.

FieldTypeNotes
idREQstringStable slug (3–32 chars, [a-z0-9-]). Cannot change between versions.
nameREQstringDisplay name shown in the marketplace and UI.
versionREQsemverMAJOR.MINOR.PATCH. Approved versions are immutable.
descriptionREQstring≤ 280 chars. Plain text, no HTML.
authorREQstringYour Casa Cloner username.
categoryREQenumautomation | utility | fun | security | other
iconUrlhttps URLOptional. HTTPS only, ≤ 1 MB, square preferred.false
permissionsstring[]See §Permissions. Anything outside the allowlist is rejected.false
hooksREQstring[]Must match the hook names actually defined in plugin.py.
04

Hooks & runtime API

Every hook receives the api object as its first argument. That object is the only legitimate way to talk to the host — there are no globally importable host modules.

pythonplugin.py
def on_load(api):
    api.log("auto-react loaded")

def on_message(api, message):
    if api.has_permission("storage.local"):
        emoji = api.storage_get("emoji", "👍")
        # ... react logic ...

def on_unload(api):
    api.log("auto-react unloaded")
Available hooks
HookSignatureWhen it fires
on_load(api)Once, immediately after the plugin is loaded.false
on_unload(api)Once, before the plugin is removed from memory.false
on_ready(api)When the desktop app finishes booting.false
on_message(api, message)Every incoming Discord message.false
on_clone_start(api, ctx)Just before a clone job starts.false
on_clone_finish(api, ctx)After a clone job ends (success or error).false
on_error(api, error)Host-level error notifications.false
api surface
  • api.slug, api.manifest, api.permissions — read-only metadata.
  • api.log(message: str) — write to the host log (truncated at 2000 chars).
  • api.has_permission(name: str) -> bool — guard sensitive paths.
  • api.storage_get(key, default=None) / api.storage_set(key, value) — JSON KV store, gated on storage.local.
05

Permissions

Permissions are declared up-front in the manifest and surfaced to the user before install. A plugin asking for a permission it didn't declare simply fails at runtime.

NameEffect
storage.localRead/write the per-plugin storage.json file.false
http.requestImport requests, urllib, urllib.request, http.client. Without it those imports raise ImportError.false
events.messageReceive on_message dispatches.false
events.cloneReceive on_clone_start / on_clone_finish.false
discord.tokenREQRead the user’s saved Discord token via api.get_discord_token(). Sensitive — only request when strictly required.
Don't over-ask
Asking for permissions you don't need is the fastest way to fail review. Be surgical.
06

Sandbox rules

Plugins run inside a custom __builtins__ with the most dangerous primitives stripped, plus an import allow-list. This is defence in depth — the real isolation is server-side review.

Removed builtins
  • eval, exec, compile
  • open — use api.storage_* instead
  • input, breakpoint
Blocked imports

The following modules cannot be imported at all:

text
subprocess, ctypes, ctypes.util, multiprocessing,
socket, ssl, smtplib, ftplib, telnetlib,
shutil, pickle, marshal
Gated imports

requests, urllib, urllib.request, and http.client only import successfully when the manifest declares http.request.

07

Publishing pipeline

STEP 01
Submit
Send your plugin.py + manifest.json through Dashboard → Plugins → Create.
STEP 02
Static check
The server runs static analysis and computes the SHA-256 of the code.
STEP 03
Review
The version enters the pending state. Admins read the source.
STEP 04
Sign
On approval the server signs with HMAC-SHA256("{pluginId}:{version}:{sha256}") and flips the version to approved.
STEP 05
Install
The desktop client downloads, recomputes the SHA-256, validates the signature, then installs. Anything off → refused.
08

Integrity guarantees

✓ Guarantee
SHA-256 binding
The desktop client recomputes the hash and refuses any payload whose digest doesn't match the metadata.
✓ Guarantee
Signature required
Downloads without a 64-char hex HMAC signature are rejected before install.
✓ Guarantee
Size cap
Code larger than 5 MiB is refused on the client to defeat zip-bomb-style payloads.
✓ Guarantee
Banned-user gate
API tokens belonging to banned users cannot fetch /api/plugins/*/download.
09

Marketplace REST API

All endpoints accept Authorization: Bearer casa_… (API token) or a session cookie. Mutating endpoints are rate-limited per IP.

MethodPathPurpose
GET/api/pluginsList approved plugins (q, category, sort).false
GET/api/plugins/[slug]Plugin detail + latest approved version.false
GET/api/plugins/[slug]/versionsAll versions visible to the caller.false
GET/api/plugins/[slug]/downloadApproved bundle (auth required, banned-user gated).false
POST/api/pluginsCreate plugin record (author only).false
POST/api/plugins/[slug]/versionsSubmit a new version for review.false
POST/api/plugins/[slug]/reportReport a malicious or broken plugin.false
10

Versioning rules

  • Approved versions are immutable. Push a new version instead of editing.
  • Bumping MAJOR implies an API/permission change and may force users to re-consent on update.
  • Yanking a version requires admin action — submit a report through support.
You're ready
Ship a first version, watch it pass review, and iterate. Most plugins land in under 200 lines of Python.

// next

Open the marketplace, or ship your first plugin.

Most plugins are under 200 lines. The review queue is fast. The sandbox keeps everyone safe.