Skip to content
CADCAMFUN
Docs

Documentation

Plugins

Extension manifests, worker-host APIs, assistant tools, and the separate browser plugin installation contract.

Choose the extension surface

@cadcamfun/plugin-sdk defines the worker plugin contract with a plugin.json manifest and API version 1. Its host exposes capability-controlled services and runs plugin functions in Bun workers. The production assistant loader currently accepts plugins that request only ai:tool.

The /api/plugins HTTP family uses a different browser-plugin contract: ZIP packages with manifest.json, a main entry, browser permissions, configuration, and JavaScript bootstrap. Installing a package through that API does not make it a worker SDK plugin. Use the manifest and lifecycle matching the intended integration.

Worker SDK manifest

plugin.json for an assistant extension
{
  "id": "com.example.shop-helper",
  "name": "Shop helper",
  "version": "1.0.0",
  "apiVersion": "1",
  "entry": "index.js",
  "capabilities": ["ai:tool"],
  "contributes": {
    "aiTools": [{
      "id": "lookup",
      "description": "Look up the shop's documented inspection checklist.",
      "kind": "read",
      "parameters": { "type": "object", "properties": {} },
      "entry": "lookup"
    }]
  }
}

Identifiers use a reverse-DNS form, the version is semantic major.minor.patch with an optional prerelease, and apiVersion must be "1". Entry paths are relative to the plugin directory. Contributions can declare commands, panels, post-processors, operations, and assistant tools; declaring a contribution does not wire that feature into every host.

The schema allows optional signature and dev fields. The current worker host records these values but does not perform cryptographic signature verification during install. A signature field alone is not evidence of a verified publisher.

Module entry and AI functions

definePlugin is a typed identity helper for a module with a manifest and activate(ctx). Activation may return a cleanup function, and the module may expose deactivate. The SDK describes these lifecycle hooks; their availability depends on the concrete worker bootstrap, as detailed below.

Assistant functions are exported through an aiTools record; the contribution's entry names the function in that record. A tool contribution includes its description, JSON parameter schema, and read or write kind. A write tool is routed through a human approval request in the embedded assistant; an outside bridge receives only permitted read plugin tools.

Bundle the worker entry as a self-contained module. The worker host reads the entry source and imports it through a blob module. Treat relative package or file imports as a separate integration concern rather than assuming a Node-style filesystem loader.

Capability-controlled APIs

The SDK recognizes drawing:read, drawing:write, toolpath:read, storage, ui:panel, and ai:tool. Drawing write implies drawing read. The general host derives effective capabilities from the installed manifest and checks them on RPC; the embedding application decides which manifests and surfaces it is willing to load.

The SDK context can expose drawing reads and Add/Update/RemoveEntity patches, toolpath/program reads, private string key-value storage, and declarative command/panel registration. Missing services are absent from the context. Storage keys use plugin:<id>: namespaces and reject absolute or parent-traversal keys.

The production assistant loader supports only ai:tool. A manifest asking for drawing, toolpath, storage, or UI capabilities is skipped with a warning. It does not silently grant those surfaces or load their broader capabilities into the assistant host.

Loading and worker lifecycle

To enable an assistant extension, place its plugin.json and bundled entry under <dataDir>/plugins/<id>/ and list the identifier in <dataDir>/plugins/enabled.json, a JSON array. The folder identifier must match the manifest. Loading occurs while the assistant plugin service is built; a failed plugin logs a warning without stopping the application.

The general host supports installed, enabled, disabled, and faulted states, with install, enable, disable, uninstall, command call, AI-tool call, and configuration operations. Enable creates a worker and completes its bootstrap handshake; disable tears it down. Uninstall removes the registry state and storage namespace through the host's injected dependencies.

Workers start with an empty environment and the bootstrap removes selected global APIs. This is an application-level restriction, not an operating-system sandbox: Bun globals remain reachable. Enable only extension code that is appropriate to run inside the application process boundary.

Runtime limits

The host defines a 256 MB worker memory setting, a 2,000-call-per-minute rate limit, and at most 64 pending calls per plugin. Command timeout behavior is supplied by the embedding host; the assistant loader uses 10 seconds. AI tool calls have a separate 30-second deadline. Timeouts, absent tools, worker crashes, capability failures, and queue exhaustion return typed host failures or readable tool results.

Crash tracking faults a plugin after three crashes within ten minutes. These host controls bound cooperative RPC and lifecycle behavior; the memory setting passed to Bun is not a substitute for a separately isolated executable.

The limits are defined in apps/sidecar/src/plugins/host.ts: CALL_TIMEOUT_MS (5,000 ms per ordinary call), AI_TOOL_TIMEOUT_MS (30,000 ms per assistant tool call), MEMORY_LIMIT_MB (256), CALL_RATE_LIMIT (2,000 calls per plugin per minute), MAX_PENDING_CALLS (64), and CRASH_LIMIT (3 crashes within a 10-minute CRASH_WINDOW_MS, after which the plugin is disabled automatically).

ZIP installation and browser plugin API

POST /api/plugins/install accepts a multipart archive. The installer reads manifest.json from the ZIP root or a single top-level directory, validates the browser-plugin manifest, and stores its files under <dataDir>/storage/plugins/<id>/. Browser manifests use main, engines.cadcam, and a browser permission list, rather than SDK entry, apiVersion, and capabilities.

GET /api/plugins lists those installed entries; detail, enable/disable, uninstall, update, and configuration endpoints operate on their registry. /api/plugins/bootstrap.js?id=… serves JavaScript; /api/plugins/serve?id=…&file=… serves plugin files. These are text/binary operations with their declared content types.

The local registry browser reads package metadata under <dataDir>/plugins-registry. It is a local manifest catalogue. Archive installation, registry listing, and worker assistant extension loading are separate operations.

SDK helpers

effectiveCapabilities, hasCapability, and missingCapabilities support capability inspection. requireCapability raises a capability error when a requested context surface is absent. storageNamespace and storageKey build namespaced keys; parseManifest validates JSON; manifestKey formats an ID/version pair.

Implementation entry points

packages/plugin-sdk/src owns the public manifest, capability, context, plugin, and test-host APIs. apps/sidecar/src/plugins/host.ts owns worker lifecycle and RPC; services/ai-plugins.ts owns consent-file loading and assistant registration. Browser installation is implemented in http/plugins.ts and repos/plugin-registry-repo.ts.

Extension review

Keep contribution schemas accurate, return small structured results, and declare read/write behavior honestly. Test against the actual embedding host: SDK availability does not establish production access to drawing, storage, UI, or post registries. The standalone nikcli plugin is an HTTP action client and has a different purpose from code loaded into a worker.

Complete the sample assistant module

The plugin.json example above names aiTools.lookup. This is the corresponding self-contained index.js. Top-level module code runs when imported; the declared lookup function runs when called.

index.js
export const aiTools = {
  lookup: async () => ({
    ok: true,
    checklist: [
      "Compare the drawing revision with the job traveller.",
      "Record the measuring instrument and its calibration status.",
      "Record each inspected dimension and the measured value."
    ]
  })
}

These are sample shop instructions, not a built-in inspection standard. Replace them with the content your extension actually owns. To admit this module at the next service initialization, the enabled file is:

<dataDir>/plugins/enabled.json
["com.example.shop-helper"]

The running catalogue exposes an identifier derived from plugin and tool IDs. Discover it through GET /api/ai/actions; do not assume the browser ZIP registry is the assistant's enabled list. Reloading the application/service is the reliable way to reread the production consent file; no filesystem watcher is configured by this loader.

Source: apps/sidecar/src/services/ai-plugins.ts, packages/ai/src/tools/plugin-tool.ts, and apps/sidecar/test/ai-plugin-tools.test.ts.

Argument schemas and lifecycle limits

A tool's local ID must start with a lowercase letter and contain lowercase letters, digits, or underscores, with a 40-character maximum. Descriptions contain 10–2,000 characters. The contribution must name a non-empty entry function and a JSON parameter schema. The application adapter validates arguments before calling the worker; a refusal can return a structured ok: false result without terminating the plugin.

The public SDK describes activate, cleanup, and deactivate entry points. The current Bun worker bootstrap's activate/deactivate RPC handlers acknowledge lifecycle messages but do not invoke those exported module functions. Assistant extensions should therefore put their callable behavior in aiTools and avoid depending on SDK activation callbacks for setup or cleanup.

The general worker bootstrap also contains placeholder drawing/toolpath/UI handlers. They do not constitute production drawing or UI integration. The assistant loader's restriction to ai:tool prevents these unsupported surfaces from being advertised as working application access.

Source: packages/plugin-sdk/src/manifest.ts, plugin.ts, apps/sidecar/src/plugins/host.ts (PLUGIN_BOOTSTRAP), and services/ai-plugins.ts.

Diagnose a missing or failing extension

  • No tools in the catalogue: check valid enabled JSON, exact folder/manifest ID match, manifest decoding, and the ai:tool capability. Read the startup warning for a skipped plugin.
  • Manifest requests unsupported surfaces: remove unsupported capabilities only if the code truly does not need them. An assistant plugin cannot acquire drawing-write access merely by declaring it.
  • NoSuchTool: make contributes.aiTools[].entry match an exported function under aiTools. The contribution ID and entry name serve different purposes.
  • Argument validation failure: inspect the JSON schema and required fields. Wrong values are refused before execution, as demonstrated by the real-worker and application integration tests.
  • Timeout or faulted state: investigate blocking work, unresolved promises, or crashes; repeated crashes fault the plugin. Re-enable only after correcting the cause.
  • ZIP installer rejects the manifest: use the browser-plugin manifest.json contract for that endpoint. Renaming a worker plugin.json does not translate its fields.

Source: apps/sidecar/test/plugins-ai-tools.test.ts, ai-plugin-tools.test.ts, plugins-http.test.ts, and src/plugins/host.ts.

Manifest reference

Every plugin ships a plugin.json. parseManifest(json) parses and validates it in one step, turning a JSON syntax error into the same error channel as a schema error, so an install screen only needs to handle one failure type.

FieldRule
idReverse-DNS identifier: 3–128 characters, lowercase letters, digits, and hyphens in at least two dot-separated labels, for example com.example.gear-generator.
nameNon-empty display name.
versionmajor.minor.patch, optionally followed by a pre-release suffix such as -beta.1.
apiVersionMust be the string "1"; it is the only plugin API version.
entryES module path relative to the plugin folder.
capabilitiesArray of capability strings (see below). Anything outside the closed list is a manifest error.
contributesOptional commands, panels, postProcessors, operations, and aiTools.
signatureEd25519 signature over the archive hash; required for packaged installs.
devtrue marks a local development plugin: signature checks are skipped and the UI shows a badge.
ContributionFields
commands[]id, title, optional keybinding such as Mod+Shift+G.
panels[]id, title, entry: an exported function that returns the panel's declarative tree. There is never DOM access.
postProcessors[]id, title, entry: an exported (program, options) => string function.
operations[]id, title, entry: an exported record with a parameter schema and a pure generate(params, geometry, tool, stock) that returns a toolpath.
aiTools[]id matching ^[a-z][a-z0-9_]{0,39}$, description (10–2,000 characters; the model reads it), kind read or write, parameters (JSON Schema of the arguments), and entry.

Helpers: manifestKey(m) returns id@version; requestedCapabilities(m) returns the sorted capability list for display; isDevPlugin(m) and isSigned(m) answer the install-policy questions.

Capabilities and the plugin context

Capabilities are the only thing a plugin can act through. The manifest declares what the plugin needs, the user grants a subset, and the host builds a context that contains exactly the granted surfaces. Each surface is optional in the type, so a missing capability is visible both at compile time and at runtime instead of becoming a silent no-op.

CapabilityContext surfaceAllowsImplies
drawing:readctx.drawing.read()Read the open drawing (or undefined when none is open).—
drawing:writectx.drawing.patch(patch)Apply AddEntity, UpdateEntity, or RemoveEntity patches; returns the new revision.drawing:read
toolpath:readctx.toolpath.list(), .program(id)Read toolpaths and G-code programs.—
storagectx.storageNamespaced key-value strings: get, set, delete, keys.—
ui:panelctx.uiregisterCommand and registerPanel (declarative only).—
ai:toolnone (manifest contributions)Offer tools to the assistant and, read-only, to a standalone nikcli.—
effectiveCapabilities(granted) resolves implications, so a plugin that declares drawing:write does not need to declare drawing:read. Every context also has pluginId, the resolved capabilities, and a log with debug, info, warn, and error; the host prefixes log lines with the plugin ID. Use requireCapability(ctx, capability, surface) to obtain a surface or fail with a CapabilityError that names the missing grant.

Test a plugin with the SDK test host

createTestHost builds an in-memory host with the same capability rules as the sidecar. Only granted capabilities appear on the context, and you can supply a drawing, toolpaths, and programs. The example below is complete and runs with bun in the workspace.
frame-plugin.test.ts
import { cad } from "@cadcamfun/core"
import { createTestHost, definePlugin, requireCapability, testManifest } from "@cadcamfun/plugin-sdk"

const manifest = testManifest({
  id: "com.example.frame",
  capabilities: ["drawing:write", "storage", "ui:panel"],
})

const plugin = definePlugin({
  manifest,
  activate(ctx) {
    const drawing = requireCapability(ctx, "drawing:write", "drawing")
    ctx.ui?.registerCommand({ id: "frame.add", title: "Add frame line" })
    ctx.storage?.set("runs", "1")
    drawing.patch({
      _tag: "AddEntity",
      entity: { ...cad.baseEntity("frame-1"), _tag: "Line",
                start: { x: 0, y: 0 }, end: { x: 100, y: 0 } },
    })                                   // returns revision 1
    return () => ctx.log.info("teardown") // runs on deactivation
  },
})

const host = createTestHost({ manifest, drawing: cad.emptyDrawing("d1", "Demo") })
const stop = await host.activate(plugin)
host.drawing()?.entities.length     // 1
host.context.storage?.get("runs")   // "1"
host.ui.commands                    // [{ id: "frame.add", title: "Add frame line" }]
await stop()

// Without the grant, requireCapability throws:
const denied = createTestHost({ manifest, granted: ["storage"] })
await denied.activate(plugin)
// CapabilityError: Plugin com.example.frame may not use "drawing:write":
// it was not granted at install time
  • testManifest(overrides) starts from a minimal valid manifest (com.example.test-plugin, version 1.0.0, API 1, entry index.js, no capabilities).
  • granted defaults to everything the manifest asks for. Pass a smaller list to test the denied paths.
  • host.storage is a snapshot taken when the host is created. Read live values through host.context.storage.
  • host.on(commandId, handler) and host.invoke(commandId, args) exercise registered commands.
  • runPostProcessor runs a contributed post against a program, and keyOf(pluginId, key) shows the namespaced storage key so tests can assert isolation.

How the application loads assistant plugins

  1. At startup the sidecar reads <data dir>/plugins/enabled.json, a JSON array of plugin IDs. Nothing is loaded unless it is listed there.
  2. For each ID it reads <data dir>/plugins/<id>/plugin.json and parses it. The folder name must equal the manifest id.
  3. Only ai:tool is wired in this host. A manifest that asks for drawing, toolpath, storage, or UI access is skipped with a warning rather than loaded with surfaces that would silently do nothing.
  4. The plugin is installed into a worker host and enabled. A plugin that fails to load logs Plugin <id> was not loaded: ... and never stops the application from starting.
  5. Each contributed tool is offered to the assistant as plugin_<slug of plugin id>_<tool id>, at most 80 characters, where the slug lowercases the ID and replaces other characters with underscores. com.example.bolt-circle with tool bolt_circle becomes plugin_com_example_bolt_circle_bolt_circle.
  6. Arguments are validated against the tool's JSON Schema before the worker sees them. If the schema exceeds 20,000 characters, is not an object schema, or cannot be converted, the host accepts any object and the plugin must validate.
  7. A call that throws, times out, or finds the plugin gone returns { ok: false, error } (error text up to 500 characters) to the assistant instead of crashing the turn. A write tool is confirmed with the person before every call, whatever its arguments say.

Not an operating-system sandbox

The worker has no process, fetch, or file handle of its own, but Bun globals remain reachable inside it. Enable only plugins whose code you trust, and keep write tools to a minimum.

Identifier and version rules, tested

ValueValid?Rule
com.example.gear-generatoryesReverse-DNS: lower-case segments of letters, digits and -, at least two, 3–128 characters
a.byesTwo one-letter segments are enough
gearnoNeeds at least one dot
Com.Example.xnoUpper case is not allowed
com.example_x.ynoUnderscore is not allowed
VersionValid?
1.0.0yes
1.2.3-beta.1yes
1.0no: three numbers required
v1.0.0no: no prefix
apiVersion must be the string "1", the only plugin API major that exists. AI tool ids must match ^[a-z][a-z0-9_]{0,39}$ and are shown to the assistant as plugin_<plugin>_<id>; descriptions must be 10–2,000 characters because the model reads them.

Contribution types

KeyFieldsPurpose
commandsid, title, optional keybinding (e.g. Mod+Shift+G)Commands in the palette
panelsid, title, entryA panel rendered from a declarative tree the named function returns; no DOM access
postProcessorsid, title, entryA (program, options) => string post registered at runtime
operationsid, title, entryAn exported { schema, generate } record
aiToolsid, description, kind read|write, parameters (JSON Schema), entryTools offered to the assistant; a write tool is confirmed before every call

Other manifest fields: signature (Ed25519 over the archive hash, required for packaged installs) and dev (local development plugin: skips signature checks and shows a badge).

Capability resolution, tested

Implication, missing grants, storage keys
import { effectiveCapabilities, missingCapabilities, storageKey, requireCapability } from "@cadcamfun/plugin-sdk"

effectiveCapabilities(["drawing:write", "storage"])
// ["drawing:read", "drawing:write", "storage"]   (write implies read; sorted)

missingCapabilities(["drawing:read", "toolpath:read", "ai:tool"], ["drawing:write"])
// ["toolpath:read", "ai:tool"]

storageKey("com.example.gear", "presets/m2")   // "plugin:com.example.gear:presets/m2"
storageKey("com.example.gear", "../x")
// RangeError: storage key "../x" may not escape the plugin namespace   (same for "/abs")

requireCapability(ctxWithoutDrawing, "drawing:read", "drawing")
// CapabilityError (_tag "CapabilityDenied"):
// Plugin com.example.gear may not use "drawing:read": it was not granted at install time
Surface on the contextPresent when grantedMethods
drawingdrawing:read or drawing:writeread() → drawing or undefined; patch(p) → new revision, with AddEntity, UpdateEntity, RemoveEntity patches
toolpathtoolpath:readlist(), program(toolpathId)
storagestorageget, set, delete, keys; string values
uiui:panelregisterCommand, registerPanel
logalwaysdebug, info, warn, error

In 3D

Plugin packages in the workspace Drag to orbit · scroll to zoom · right-drag to pan
All 13 workspace packages from their package.json files; each line is a workspace:* dependency (17 in total). Rows are dependency depth: packages with no workspace dependencies at the front, the sidecar, which depends on 8 of them, at the back. Orange boxes are apps. Highlighted: plugin-sdk (depends on core) and the nikcli plugin, both loaded by the sidecar.