Files
cletus-plugins/docs/specification.md
T

284 lines
13 KiB
Markdown

# 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` | `<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 <domain> | 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": <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_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
`<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.
```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 `<plugin>:<service>.<method>` or
`<plugin>:<service>.*`.
- `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 |