# Cletus plugin specification Draft 1, 2026-11-10. Host API version 2. ## Parts of a plugin | Part | Language | Runs | Required | | --- | --- | --- | --- | | Worker | Go | as its own process, started and stopped by Cletus | yes | | Chat tools | Go, in the worker | when Cletus's AI calls them, in chat or background runs | no | | Screen | TypeScript (Svelte 5 supported) | in a sandboxed frame inside the Cletus window | no | | Services | Go, in the worker | when another plugin calls them through the host | no | The worker does the work. The screen shows it. The host (Cletus) sits between them and everything else: the user's files and shared folders, background agent runs, other plugins. The plugin never holds the user's Cletus key or any organization credential. ## The repository ``` plugin.json identity, permissions, tools, services CHANGELOG.md one entry per version, written for users go.mod, go.sum the worker module (CGO_ENABLED=0) vendor/ go mod vendor output; the build has no network cmd/worker/ the worker's main package internal/ the rest of the worker testdata/ synthetic files for tests and `cletus-plugin run` docs/plan.md purpose, permissions and dependencies, each with a reason docs/guide.md the user guide, shown on the store listing listing/ listing.json for the store ``` Eidonix builds the worker for macOS (arm64) and Windows (amd64) with the Go version the SDK pins, with `CGO_ENABLED=0`. `cletus-plugin check` builds the same two targets locally. ## plugin.json ```json { "id": "acme.gauge-check", "name": "Gauge Check", "description": "Check measured gauges against the drawing's tolerances.", "version": "1.2.0", "min_app_version": "0.5.0", "host_api": 2, "color": "teal", "permissions": ["files", "workspace.read"], "file_types": [".gauge"], "opens_folders": false, "tools": [ {"name": "gauge_check_run", "mutating": false}, {"name": "gauge_check_save_report", "mutating": true, "requires_approval": true} ], "provides": {}, "uses": ["cad-viewer:cad.open"], "dependencies": [] } ``` | Field | Rules | | --- | --- | | `id` | `.`. Eidonix assigns your publisher id. ``: lowercase letters, digits, hyphens. Never changes. | | `name`, `description` | Shown in the store and the app. Plain words. No claims about Eidonix or other companies. | | `version` | Semantic version, raised on every submission. Never reused. | | `min_app_version` | The oldest Cletus with every host method you call (`host-api.md` lists each method's version). | | `host_api` | `2`. | | `color` | One of `red orange amber yellow green teal blue indigo violet pink grey`. The app maps it to each theme. | | `permissions` | See "Permissions". Undeclared host methods are refused. | | `file_types` | Extensions your screen opens (with the screen toolchain) ("Open in …"). Claim your own formats. Common formats (`.xlsx`, `.pdf`, `.step`) are allowed when the plugin really works on them; the user picks which plugin opens a file. | | `opens_folders` | `true` if your screen opens a whole folder. | | `tools` | Every chat tool. `mutating: true` if it writes anything anywhere. `requires_approval: true` makes Cletus ask the user each time. Must match what the worker reports. | | `ui`, `ui_host_api` | The screen and the SDK modules it uses. Defined with the screen toolchain; leave them out until then. | | `provides`, `uses` | Services offered to other plugins, and theirs you call. See "Services". | | `dependencies` | Plugins that must be installed for yours to work (for example `cad-viewer`). The user installs them; nothing is bundled. | ## Permissions Shown to the organization's admin when they turn your plugin on, and to the user at install, in the plain words below. An update that adds a permission is reviewed again and must be approved again by each organization. **App** means Cletus refuses calls you didn't declare. **Review** means the worker could technically do it, so review checks that the code does only what `docs/plan.md` says. | Permission | Plain words | Grants | Enforced by | | --- | --- | --- | --- | | (always) | | Chat tools, own data folder, own screen, events, dialogs, published data links | app | | `files` | Open and save files you choose | `host.files.*`: files the user opened, dropped or picked, and the working folder within its write rules | app + review | | `files.computer` | Read and write other files on this computer | Direct access to the folders `docs/plan.md` names | review | | `workspace.read` | Read files in shared folders you're a member of | `host.workspace.folders / list / fetch / read` | app | | `workspace.write` | Add, change and delete files in shared folders | `host.workspace.put / delete / create / rename` | app | | `agents` | Run Cletus in the background | `host.agent.*` | app | | `chat.handoff` | Send work to chat | the screen's `handoffToChat` | app | | `identity` | See your name and organization | `host.auth.whoami` | app | | `net:` | Connect to | Direct connections from the worker to that host and its subdomains; at most 16 | review | | `net.configured` | Connect to devices and servers you set up in this plugin | Direct connections to addresses the user enters in the plugin (controllers, databases, instruments) | review | | `processes` | Run programs on this computer | Starting the programs `docs/plan.md` names | review | Permissions for the planned host services (AI models, vision, speech, camera and microphone, saved connections, a secret store) will be added with them. Until then, a plugin that needs a password asks the user each time and keeps it in memory only. **Never granted**, with or without review: the user's Cletus key or session; organization or provider credentials; vault keys or vault contents; other plugins' data; Cletus's settings and stored secrets; the user's chat history and memory; the commands of Cletus Code or System Administration; an organization's managed libraries. ## The worker Cletus starts the worker on first use and stops it when the plugin is disabled, updated or uninstalled, or the user signs out. It restarts a crashed worker on the next call. The worker gets: - `CLETUS_PLUGIN_ID`: your plugin id; - `CLETUS_PLUGIN_DATA`: its **data folder**, per signed-in account, kept across updates and uninstalls, and also its working directory. Settings, caches, history and job state go there (`pluginsdk.DataDir()`); - a few standard variables (`HOME`, `PATH`, temp folders, locale). Don't rely on any other variable; - standard input and output for messages, standard error for its log (`worker.log` in the data folder; never log user data). The SDK's `pluginsdk.Serve` implements the protocol below. You register handlers. ```go func main() { err := pluginsdk.Serve(pluginsdk.Handlers{ Tools: []pluginsdk.Tool{countLines}, // chat tools UI: map[string]pluginsdk.Func{"check": check}, // answers ui.check Services: map[string]pluginsdk.ServiceFunc{"gauge.check": gauge}, // answers svc.gauge.check Busy: jobs.Running, }) if err != nil { os.Exit(1) } } var countLines = pluginsdk.Tool{ Name: "gauge_check_count_lines", Description: "Count the lines in a text file. Returns the count.", Parameters: map[string]any{"type": "object", "properties": map[string]any{"path": map[string]any{"type": "string"}}, "required": []string{"path"}}, Run: func(ctx context.Context, h *pluginsdk.Host, call pluginsdk.ToolCall) pluginsdk.ToolResult { var args struct{ Path string `json:"path"` } if err := call.Decode(&args); err != nil { return pluginsdk.Fail("%v", err) } path, err := h.ResolvePath(ctx, args.Path) // host.files.resolve if err != nil { return pluginsdk.Fail("%v", err) } // … read path … return pluginsdk.Text("%s has %d lines.", path, n) }, } ``` `h.Call(ctx, method, params, &out)` makes any call in `host-api.md`; `ResolvePath`, `CanWrite`, `Event` and `Publish` are typed shortcuts. `pluginsdk.NewTestHost` answers host calls in unit tests. The template that `cletus-plugin new` writes shows all of this working. ### Protocol One JSON object per line in each direction, at most 64 MiB per line. ```json {"id": "7", "method": "tools.invoke", "params": {}} {"id": "7", "result": {}} {"id": "7", "error": {"message": "The drawing has no tolerance table."}} ``` A host call made while handling request `7` carries `"parent_id": "7"`. Cancellation is `{"method": "$/cancel", "params": "7"}`: stop, and reply with an error. Stop promptly: a worker that ignores cancellation may be stopped. | Method the worker answers | Params | Result | | --- | --- | --- | | `plugin.handshake` | none | `{"api_version": 1}` | | `plugin.shutdown` | none | `{"ok": true}`, then exit | | `plugin.busy` | none | `true` while work must not be interrupted; Cletus then won't update or stop the plugin | | `tools.list` | none | `[{"def": , "mutating": bool, "requires_approval": bool}]` | | `tools.invoke` | `{"name", "args", "working_dir", "edit_mode"}` (`args` is JSON text) | `{"Text": "…"}` or `{"Error": "…"}`; `"Images": [base64]` optional | | `ui.` | the screen's arguments, as a JSON array | any JSON; answer within 60 seconds (start longer work as a job and report it with events) | | `svc..` | `{"caller", "mutating", "args"}` | must match the declared `result` schema | ### Chat tools The AI reads your tool definitions and results, so write them for it: - One tool does one thing. Name it `_`. - The description says what it does, when to use it, and what it returns. - Results are short and factual. A failure says why and what to call next. Never try a different method silently. - When `edit_mode` is false, the user has Cletus read-only: a mutating tool asks `host.files.can_write` for each path and refuses if the answer is no. The same tools run in background agent runs, where nobody watches. Don't open dialogs from a tool. ## The screen **The screen toolchain (the `@cletus/plugin-sdk` package, its build and a preview host) arrives in a later SDK release.** Until then, build the worker and chat tools; a plugin with tools only is complete and publishable. Design your screen with the rules below so it drops in when the toolchain ships. - The screen runs in a sandboxed frame with no network access. It cannot see the Cletus window or other plugins. - It calls only its own worker: `call('check', arg)` reaches the worker's `ui.check` method, with the arguments as a JSON array. - Events from the worker (`host.event`) reach the screen only under your own `:` prefix. - Large data (meshes, images, tables) is published by the worker (`host.data.publish`) and fetched by the screen from the returned link. Every other request is blocked. - Settings: up to 512 KiB, stored by the host. - WebGL, WebGPU, Web Workers, canvas, clipboard writes and downloads of your published data work. The camera and microphone don't. - Cletus draws the menu bar and the ← Chat button above the screen. - Colours, density and layout follow `style-guide.md`. ## Services Plugins share no code. They call each other's declared services through the host, which checks both sides. ```json "provides": { "gauge": {"methods": {"check": { "description": "Check a gauge file against its drawing.", "params": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], "additionalProperties": false}, "result": {"type": "object"}, "mutating": false, "long": false }}}, "screen": {"methods": {"open": {"params": {"type": "object"}, "result": {"type": "null"}}}} }, "uses": ["cad-viewer:cad.open", "acme.fixtures:fixture.*"] ``` - Services are lowercase and may be dotted (`gauge.batch`). Methods are `[a-z][a-z0-9_]*`. A use is `:.` or `:.*`. - `params` and `result` are JSON schemas (draft 2020-12 or draft-07, at most 16 KiB, no remote `$ref`). The host checks arguments and results against them. - `screen.open` is reserved. Provide it if other plugins may open your screen. Its `params` is the payload your `open()` receives. - Calls are limited to 5 per second after a burst of 20 per pair, 1 MiB of arguments, and 2 minutes (15 minutes for methods marked `long`). - A mutating method linked to a tool that needs approval is refused when called from another plugin, because the user can't be asked. - Services between your own plugins work the same way. Services of Eidonix plugins are not open to publishers yet; they will be listed in `host-api.md` when they are. ## Versions and data - Users choose when to update and can go back to any earlier version. Keep every file you write readable after a downgrade, or refuse it with a plain message naming the version that wrote it. - Uninstalling keeps the data folder. Reinstalling picks it up. - `min_app_version` protects users on older Cletus releases. Raise it whenever you use a newer host method. ## Limits | Limit | Value | | --- | --- | | Message | 64 MiB per line | | Service call | 1 MiB arguments; 2 minutes, or 15 if `long` | | Background agent runs | 4 queued or running per plugin | | Published data link | 1 hour at most | | Screen storage | 512 KiB | | `workspace.read` | 2 MiB per call (use `fetch` for larger files) | | Package | 768 MiB download, 3 GiB unpacked, 20,000 files |