Automate

Plugins

Add your own actions to a space: an HTTPS service in any language, or a command on your own machine through a runner. Signed jobs, ranged reads of the input, results as sidecars and files.

A plugin adds actions to Side Space: a QC check, your own transcription service, an upscaler on your GPU. Its code never runs on the Side Space server. It is either an HTTPS service that receives signed jobs, or a runner: a command on a machine of yours that claims jobs, with no public address needed.

Once a space manager installs it, its actions appear as x-acme.qc.check next to the built-in ones. They can be run by hand, used in pipelines, called by agents over MCP, and are cached by content the same way (within the space, and run again when the plugin's version changes).

The manifest

JSON
{
  "name": "x-acme.qc",
  "title": "Acme QC",
  "version": "1.2.0",
  "endpoint": "https://qc.acme.example/side/jobs",
  "timeout_seconds": 1800,
  "actions": [
    {
      "name": "check",
      "title": "Check for dropped frames",
      "accepts": ["video/"],
      "writes": ["x-acme.qc"],
      "params_schema": { "type": "object", "properties": { "strict": { "type": "boolean" } } }
    },
    { "name": "report", "title": "Make a QC report", "accepts": ["video/"], "writes": ["x-acme.qc", "files"] }
  ]
}
  • name starts with x-; actions are called <name>.<action>.
  • accepts lists MIME prefixes (empty means any file). Params are checked against params_schema before a job is sent.
  • writes is everything the action may write: x-… namespaces, and if declared transcript, ai.tags, ai.generation, user and files (new files, which get a derived sidecar). A result that writes anything else fails as a whole and writes nothing.
  • version is part of the cache key: a new version processes files again.
Shell
sidespace plugin install "Film A" https://qc.acme.example/side/manifest.json   # prints the signing secret, once
sidespace plugin list "Film A"
sidespace run "Film A" /Ep01/a.mov x-acme.qc.check --param strict=true
sidespace plugin enable "Film A" plg_… --off
sidespace plugin rm "Film A" plg_…

HTTPS plugins

The server POSTs a job to endpoint, signed like a webhook (Side-Signature, with the secret from install):

JSON
{
  "type": "run",
  "run_id": "run_…",
  "action": "check",
  "attempt": 1,
  "space_id": "spc_…",
  "node_id": "nod_…",
  "version_id": "ver_…",
  "path": "/Ep01/a.mov",
  "name": "a.mov",
  "mime": "video/quicktime",
  "size": 734003200,
  "params": { "strict": true },
  "sidecars": { "media": { "duration_s": 92.4, "width": 3840, "height": 2160, "fps": 24 } },
  "input": { "url": "https://blocks.side.example.com/files/ver_…?t=…", "expires_at": "…" },
  "callback": { "url": "https://side.example.com/v1/plugin-runs/run_…", "token": "srt_…", "expires_at": "…" }
}

input.url reads the version's bytes with Range support, valid for the whole timeout, so you can read only the part you need. Answer within 60 seconds:

AnswerMeans
200 with a result {"sidecars": {"x-acme.qc": {…}}, "files": […]}Done
202You're working in the background; finish through the callback
4xxThe run fails with the reason you give
5xx or a timeoutRetried like any action

After a 202, call the callback with Authorization: Bearer <callback.token>:

  • POST {callback.url}/progress with {"progress": 0.4}
  • POST {callback.url}/upload for an upload ticket, then POST the file to the gateway's /stream-upload to get its manifest
  • POST {callback.url}/complete with the result, or {"error": "…", "retry": true}

If a run isn't completed within timeout_seconds, it is sent again, up to three times.

import { PluginRun } from "@sidespace/sdk";
import { verifyWebhookSignature } from "@sidespace/sdk/webhooks";

// In your HTTP handler, with the raw body:
if (!verifyWebhookSignature(secret, req.headers["side-signature"], rawBody)) return res.writeHead(401).end();
res.writeHead(202).end();

const run = new PluginRun(JSON.parse(rawBody));
await run.progress(0.5);
const manifest = await run.upload(reportBytes);
await run.complete({ sidecars: { "x-acme.qc": { passed: true } }, files: [{ path: "/Ep01/a.qc.pdf", manifest }] });

A complete reference plugin using only the Python standard library (answering at once, answering 202 and calling back, uploading a file, checking signatures) is in the repository at tests/e2e/fakeplugin.py.

Runners: plugins on your own machine

When you'd rather not run an HTTPS service, or the work needs local models and GPUs (whisper.cpp, a local vision model, grading tools), set "runtime": "runner" instead of an endpoint and run this on that machine:

Shell
sidespace plugin install "Film A" ./local-tools.json         # note the plg_… id and whsec_… secret
sidespace runner --plugin plg_… --secret whsec_… --jobs 2 -- ./qc.sh
  • The runner long-polls the server for jobs, so the machine needs no public address: behind NAT or on a laptop is fine. Jobs queue while it is away.
  • Your command runs once per job. The input file is already downloaded at SIDE_INPUT; the job JSON is in SIDE_JOB (and on stdin); SIDE_ACTION and SIDE_PARAMS hold the action and params; write new files into SIDE_OUTPUT.
  • Print the result JSON on stdout. The runner uploads the files it names and completes the run:
JSON
{ "sidecars": { "x-acme.qc": { "passed": true } }, "files": [{ "path": "/Ep01/qc.pdf", "file": "qc.pdf", "mime": "application/pdf" }] }
  • progress 0.4 (or progress: 40%) on stderr shows as progress; other output is passed through. Exit code 75 means "try again later"; any other non-zero code fails the run with the last lines of stderr as the reason.
  • If no runner claims a job within timeout_seconds, it is queued again, and fails after three tries saying no runner came. plugin list shows when a runner last checked in.

Runners in other languages call POST /v1/plugin-runs/claim?wait=25 with Authorization: Bearer plg_…:whsec_…, get the same job an HTTPS plugin would, and report through its callback.