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.
def on_load(api):
api.log("hello from %s" % api.slug)
def on_message(api, message):
if message.content == "!ping":
api.log("pong")Chapters
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.
plugin.py and manifest.json on your machine.The smallest plugin that can possibly work:
def on_load(api):
api.log("hello from plugin '%s'" % api.slug)
def on_message(api, message):
if message.content == "!ping":
api.log("pong"){
"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"]
}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.
| Field | Type | Notes | |
|---|---|---|---|
idREQ | string | Stable slug (3–32 chars, [a-z0-9-]). Cannot change between versions. | |
nameREQ | string | Display name shown in the marketplace and UI. | |
versionREQ | semver | MAJOR.MINOR.PATCH. Approved versions are immutable. | |
descriptionREQ | string | ≤ 280 chars. Plain text, no HTML. | |
authorREQ | string | Your Casa Cloner username. | |
categoryREQ | enum | automation | utility | fun | security | other | |
iconUrl | https URL | Optional. HTTPS only, ≤ 1 MB, square preferred. | false |
permissions | string[] | See §Permissions. Anything outside the allowlist is rejected. | false |
hooksREQ | string[] | Must match the hook names actually defined in plugin.py. |
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.
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")| Hook | Signature | When 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 surfaceapi.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 onstorage.local.
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.
| Name | Effect | |
|---|---|---|
storage.local | Read/write the per-plugin storage.json file. | false |
http.request | Import requests, urllib, urllib.request, http.client. Without it those imports raise ImportError. | false |
events.message | Receive on_message dispatches. | false |
events.clone | Receive on_clone_start / on_clone_finish. | false |
discord.tokenREQ | Read the user’s saved Discord token via api.get_discord_token(). Sensitive — only request when strictly required. |
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.
eval,exec,compileopen— useapi.storage_*insteadinput,breakpoint
The following modules cannot be imported at all:
subprocess, ctypes, ctypes.util, multiprocessing,
socket, ssl, smtplib, ftplib, telnetlib,
shutil, pickle, marshalrequests, urllib, urllib.request, and http.client only import successfully when the manifest declares http.request.
pending state. Admins read the source.HMAC-SHA256("{pluginId}:{version}:{sha256}") and flips the version to approved.All endpoints accept Authorization: Bearer casa_… (API token) or a session cookie. Mutating endpoints are rate-limited per IP.
| Method | Path | Purpose | |
|---|---|---|---|
| GET | /api/plugins | List approved plugins (q, category, sort). | false |
| GET | /api/plugins/[slug] | Plugin detail + latest approved version. | false |
| GET | /api/plugins/[slug]/versions | All versions visible to the caller. | false |
| GET | /api/plugins/[slug]/download | Approved bundle (auth required, banned-user gated). | false |
| POST | /api/plugins | Create plugin record (author only). | false |
| POST | /api/plugins/[slug]/versions | Submit a new version for review. | false |
| POST | /api/plugins/[slug]/report | Report a malicious or broken plugin. | false |
- Approved versions are immutable. Push a new version instead of editing.
- Bumping
MAJORimplies an API/permission change and may force users to re-consent on update. - Yanking a version requires admin action — submit a report through support.
// 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.