Docs · Extend

Build a plugin

A runtime plugin is one module plus a manifest, written against the plugin SDK. It passes a static validator, runs in a locked-down Deno process and does nothing until an Owner activates it.

Most plugins are written by the AI on your site: you describe what you need, the AI writes the plugin and submits it, and you approve it. This page describes what such a plugin looks like, what the validator rejects and the limits of the sandbox. The same SDK is used by the plugins that ship with Caelo.

How a plugin arrives

  • Written by the AI. Ask in the chat, for example: "Build a plugin that lets visitors sign up for events, with name, email and event." The AI calls submit_plugin with a manifest and the source. The validator runs. On errors the AI gets structured hints, fixes the source and resubmits in the same turn. On success the plugin waits for your approval.
  • Uploaded by the Owner. A package built elsewhere is uploaded at /settings/plugins/installations as a JSON file { "manifest": …, "source": "…" }.
  • Shipped with a release. Plugins in packages/plugins/<slug>/ change through a reviewed pull request and a Caelo release. The in-product AI never edits them.

The shape

A runtime plugin is one JavaScript or TypeScript module plus a JSON manifest. The module's default export is the plugin definition. Its slug and version must match the manifest.

import { definePlugin } from "@caelo-cms/plugin-sdk";

export default definePlugin({
  slug: "event-rsvp",
  version: "1.0.0",
  tier: 2,
  schema: {
    signups: { event_id: "uuid", name: "string", email: "string" },
  },
  // Operations a visitor may call through the API gateway.
  // Default deny: anything not listed is unreachable from the public site.
  publicOperations: ["signup"],
  operations: {
    signup: async (ctx, args) => ctx.query.insert("signups", args),
    list: async (ctx) => ctx.query.list("signups", { limit: 100 }),
  },
});

Tables declared in schema live in the public database, scoped to the plugin by row-level security. ctx.query offers insert, list, update, delete and compareAndSwap. list takes since, limit (default 100, max 1000), orderBy and orderDir. All values are parameterised; no SQL crosses the boundary.

Asking for more than the base

A plugin that needs more declares it in the manifest under requestedCapabilities and explains each request in capabilityReasons. Such a plugin is reviewed at /settings/plugins/installations. A request without a reason is refused. Two examples:

  • cms_admin_schema: private author-side tables, declared as adminSchema. Visitors never reach them.
  • chat_runner_tools: operations offered to the AI as tools, declared as tools. Each tool name must be <slug_with_underscores>__<name>, and the Owner sees every name and description before approving.

What can be granted today, and what cannot, is on Plugin permissions.

What the validator rejects

An oxc-parser based validator runs before every load and rejects:

  • imports of anything other than @caelo-cms/plugin-sdk and @caelo-cms/plugin-component-kit
  • require in any spelling, import.meta and dynamic import()
  • fetch, XMLHttpRequest and WebSocket
  • any reference to Deno.*
  • strings containing SQL statements (such as SELECT … FROM or INSERT INTO)
  • eval, Function and new Function
  • top-level writes to globalThis

Bundle any other dependency into the module before submitting.

The sandbox

Runtime-installed plugins run their operations and static rendering in a separate Deno process. It starts with file, network, environment, subprocess, FFI and system access denied, and without npm or remote imports. The SDK handles are the only way out, and every call goes through the host's broker, which checks it.

LimitValue
Time per operation30 seconds (240 seconds for plugins granted image generation)
V8 heap256 MB
Protocol message1 MiB
SDK calls per operation1024
Concurrent sandboxed invocations per host4

Split long work into bounded operations and await every SDK call. A timeout does not undo a write the host already accepted. The admin and gateway images ship Deno. For local development, put Deno on your PATH or set CAELO_DENO_BINARY; a missing executable fails the call with an explicit error.

Plugins that ship with Caelo do not run in this sandbox today. See Plugins.

The visitor side: Web Components

A plugin's frontend is a Web Component in Shadow DOM, open mode by default. Plugin CSS does not leak into the page and the page does not leak into the plugin. Theme tokens arrive as CSS custom properties on the shadow root.

import { defineComponent } from "@caelo-cms/plugin-sdk";

export const component = defineComponent({
  tag: "caelo-event-rsvp",
  async mounted(host, { theme, visitor }) {
    const root = host.shadowRoot ?? host;
    root.innerHTML = "<form>…</form>";
  },
});

The static generator can bake an initial render at deploy time. The component then fetches only what changed since the build through the plugin's own read surface.

Conditional writes

ctx.query.compareAndSwap(table, id, expected, patch) changes a row only while every expected value still matches. It returns false when the row changed, disappeared or does not belong to the plugin. expected and patch take 1 to 64 declared columns; id cannot be patched. Write a fresh revision token on every successful swap, so a value that returns to an earlier state cannot revive a stale write.

Activation and updates

  • Nothing of a plugin runs until a human Owner activates it. The AI can submit and propose, never activate.
  • Approval is bound to the exact source and manifest. If the package changes while it is under review, review the new version.
  • An active package cannot be overwritten. A new version is a new package and is approved again; the running version keeps running meanwhile. Removing columns or changing their types is refused.
  • Disabling stops new calls at once and keeps the data.

Next

Something wrong or missing on this page? Open an issue on GitHub