13 KiB
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
{
"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 |
<publisher>.<name>. Eidonix assigns your publisher id. <name>: 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:<domain> |
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.login the data folder; never log user data).
The SDK's pluginsdk.Serve implements the protocol below. You register
handlers.
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.
{"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": <function definition>, "mutating": bool, "requires_approval": bool}] |
tools.invoke |
{"name", "args", "working_dir", "edit_mode"} (args is JSON text) |
{"Text": "…"} or {"Error": "…"}; "Images": [base64] optional |
ui.<name> |
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.<service>.<method> |
{"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
<plugin short name>_<verb>. - 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_modeis false, the user has Cletus read-only: a mutating tool askshost.files.can_writefor 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'sui.checkmethod, with the arguments as a JSON array. - Events from the worker (
host.event) reach the screen only under your own<id>: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.
"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<plugin>:<service>.<method>or<plugin>:<service>.*. paramsandresultare JSON schemas (draft 2020-12 or draft-07, at most 16 KiB, no remote$ref). The host checks arguments and results against them.screen.openis reserved. Provide it if other plugins may open your screen. Itsparamsis the payload youropen()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.mdwhen 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_versionprotects 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 |