Documentation / Plugins

Plugins

Package an executable behind Cairn’s validated manifest, bounded native contributions, and capability grants.

Package layout

<CAIRN_HOME>/plugins/
└── com.example.cairn.hello/
    ├── plugin.json
    └── plugin.sh

When CAIRN_HOME is unset, the plugin root is ~/.cairn/plugins. Each visible child must be a real directory, not a symlink; its name must equal the manifest ID. Manifests are capped at 1 MiB.

Example manifest

{
  "activationEvents" : [
    "onCommand:hello",
    "onPanel:status"
  ],
  "arguments" : [

  ],
  "contributes" : {
    "commands" : [
      {
        "description" : "Exercise a long-running Bash plugin over JSON-RPC.",
        "id" : "hello",
        "systemImageName" : "hand.wave",
        "title" : "Hello from Bash"
      }
    ],
    "panels" : [
      {
        "buttons" : [
          {
            "id" : "refresh",
            "role" : "primary",
            "title" : "Refresh"
          }
        ],
        "id" : "status",
        "rows" : [
          {
            "label" : "Runtime",
            "value" : "Bash"
          },
          {
            "label" : "Transport",
            "value" : "JSON-RPC 2.0 over NDJSON"
          }
        ],
        "status" : {
          "level" : "success",
          "text" : "Ready"
        },
        "systemImageName" : "terminal",
        "title" : "Bash plugin status"
      }
    ]
  },
  "executable" : "plugin.sh",
  "id" : "com.example.cairn.hello",
  "manifestVersion" : 1,
  "name" : "Cairn Bash Example",
  "permissions" : [
    "api.read"
  ],
  "version" : "1.0.0"
}

Lifecycle and wire protocol

  1. Discovery. Cairn validates the manifest, contribution bounds, relative executable, and executable permission. Invalid plugins are logged and ignored.
  2. Lazy activation. onStartup starts during app startup; command and panel processes start only when their declared contribution is used.
  3. Long-lived process. Cairn sets the plugin directory as the working directory, passes manifest arguments directly, and exposes CAIRN_PLUGIN_ID, CAIRN_API_VERSION, and CAIRN_PLUGIN_TRANSPORT=json-rpc-2.0+ndjson+stdio.
  4. Bidirectional JSON-RPC. Cairn calls plugin/activate, plugin/command, plugin/panelAction, and plugin/shutdown. A plugin may call granted Cairn API methods over the same stdin/stdout stream.
  5. Isolation. Invalid records, records over 4 MiB, and unexpected exits fail only that plugin process. Standard error is captured in bounded diagnostic chunks.
OperationTimeout
Activation30 seconds
Command or panel action5 seconds
Graceful shutdown1 second

After an unexpected exit, retries back off at 1, 2, 4, 8, 16, 32, then 60 seconds. A reload or grant change stops the old process and creates a fresh authenticated session.

Native contributions

Plugins describe content while Cairn owns rendering, focus, placement, accessibility, and action dispatch. API 1.0 does not accept arbitrary HTML, JavaScript, AppKit views, shaders, or layout code.

Command palette

A plugin may declare up to 128 commands. Each command has an ID, title, optional description, and optional SF Symbol name. Selecting one activates the plugin and sends plugin/command.

Dock panels

A plugin may declare up to 32 compact panels. Cairn renders their title, SF Symbol, rows, status, and buttons, then sends plugin/panelAction when a button is used.

Panel data model

ElementBoundsBehavior
rows32; labels 120 bytes, values 1,000 bytesRead-only label/value content rendered by Cairn.
statusOne; 500-byte textNative neutral, success, warning, or error treatment.
buttons8; unique IDsNormal, primary, or destructive native buttons.
systemImageName120 bytesSF Symbol requested for the panel or command.

Contribution IDs are at most 80 ASCII letters, digits, dots, dashes, or underscores. A command or panel is rejected unless its matching activation event is declared.

Permissions and grants

Declaring a permission in plugin.json defines the maximum capability the plugin may request; it does not grant authority.

  1. The manifest declares the maximum capabilities the plugin may request.
  2. A user or trusted local client authenticates with plugins.manage and sets an explicit grant subset.
  3. Cairn persists grants per plugin, intersects them with the current declaration and live API catalog, and authenticates the plugin session with only that result.
  4. The central router checks the required capability before every plugin-originated API call. Undeclared grants are rejected.
./scripts/cairnctl call plugins/grants/set   '{"pluginId":"com.example.cairn.hello","capabilities":["api.read"]}'   --capability plugins.manage

Changing grants stops a running plugin so its next activation receives a new session. Removing a permission from the manifest also removes its effective authority even if old persisted data exists.

Current capabilities

CapabilityAllows
api.readInspect the API catalog and service health.
events.subscribeSubscribe to and receive developer API events.
plugins.manageReload plugins and change explicit capability grants.
plugins.readInspect installed plugins, contributions, and grants.