Plugin standards
Every rule a Plugin follows, whether it is required or recommended and what enforces it, plus the checklist before you publish.
These are the rules every Plugin follows. "Must" means Studio or a check refuses the Plugin otherwise, or the rule is an owner decision. "Should" means a reviewer will ask for it. The last column says what enforces the rule; "review" means nothing automatic does yet.
The rules collect what the other pages explain. When a rule here and a page disagree, the code wins, then this page; please fix the page.
Identity and names#
| Rule | Level | Enforced by |
|---|---|---|
A Plugin id is lower-case and dotted: <owner>.<name>, such as witwin.radar or acme.fdtd. Each segment uses only a-z, 0-9 and _. | must | the manifest only checks that id is a string, but an id that breaks this cannot register RPC methods or topics (app/rpc/registry.py _check_name) |
The prefix studio. belongs to system Plugins. A third-party Plugin never uses it. | must | a third-party Plugin with a system Plugin's id is dropped (plugins/loader.py); review for other studio.* ids |
The Python distribution is wt-<domain> and declares the entry point group witwin.plugins. | must | witwin add, the shared setup.py |
RPC methods and topics are <plugin id>.<verb> (more segments allowed), lower-case snake_case segments: witwin.radar.solve_frame. | must | app/rpc/registry.py _check_name refuses other names and names outside the Plugin's own <plugin id>.* |
Agent Tools are named <plugin id>.<name>; extra Tool namespaces are declared in contributes.tools and must be dotted lower-case. | must | ctx.tools, plugins/manifest.py _TOOL_NAMESPACE |
Python events (ctx.events.publish) and web events (events.emit) start with <plugin id>.. | must | plugins/events.py; the web events service throws for a third-party Plugin |
Panel ids are <plugin id> or <plugin id>.<name>. | must | buildHost.tsx registerPanel |
A Plugin's own witwin:// URI kinds and chat-link kinds are <plugin id>.<name>. | must | the web opener service and registerChatLink refuse other kinds |
Solvers are declared as <plugin id>.<name>, so ctx.solvers finds them. | must | api/solver_api.py prefixes ids that lack it |
Command ids, setting keys and slot ids start with the Plugin id: witwin.example_notes.reload. | should | review; not checked |
Component names are PascalCase (Material, RadarSensor); component field keys are snake_case (display_name). | must | component registry, review |
| Python payload keys are snake_case. TypeScript names are camelCase. Convert once, at the transport boundary of the web half; never accept both spellings in UI code. | must | review |
| Use the canonical words: Project, Scene, Script, Plugin, Session, Run, Result. Never "workspace", "artifact" or "extension" as domain words. | must | review (dev_docs/CONCEPTS.md) |
Versions and compatibility#
| Rule | Level | Enforced by |
|---|---|---|
version in witwin.toml is SemVer and equals the distribution version. | must | the shared setup.py takes the version from witwin.toml |
Declare the Python Plugin API range: [engines] witwin = "^2.0.0". | must | plugins/loader.py _check_engine refuses a Plugin outside the range |
A web half declares sdkVersion = "2.0.0" under [contributes.viewport.host] and default-exports definePlugin({...}). SDK 1 modules (export function activate) are refused. | must | app/pluginHost/webHalves.ts; the catalog skips such versions |
Every Python requirement in witwin.toml is also in pyproject.toml. Heavy requirements (CUDA torch, solver wheels) go in the solve extra. | must | the shared setup.py fails the build |
| Plugins are never published to or installed from PyPI. Sources are a local folder, a git URL or the witwin index. | must | owner rule; witwin add pins first-party names to the witwin index |
A template's version equals its content manifest's templateVersion, and the declared sha256 matches the manifest file. | must | witwin.studio _templates.py refuses the template |
Code boundaries#
| Rule | Level | Enforced by |
|---|---|---|
A Python half imports only witwin.studio, witwin_server.api (and witwin_server.api.testing in tests), and the api module of a Plugin it lists in [[pluginDependencies]]. | must | server/scripts/plugin_api_boundary.py, copied into every Plugin repo as tests/test_plugin_api_boundary.py |
A solver child imports witwin.studio.solver and never witwin_server. | must | review; the child has no witwin_server on its path in a Project environment |
A web half imports only @witwin/host (and @witwin/host/proposed when allowed), @witwin/ui, @witwin/workbench, react, lucide-react and its own files. It never bundles its own React. | must | npm run lint:architecture for system Plugins; review for third-party ones |
A web half makes no network or desktop calls of its own: no fetch(, new WebSocket, window.witwin, localhost, navigator. sniffing. It uses the rpc, resources and platform services. | must | npm run lint:architecture for system Plugins; review for third-party ones |
A Plugin exposes to other Plugins only what its api.py (Python) or a slot / service (web) offers. No Plugin imports another Plugin's private modules. | must | the boundary checks (the Python check recognises other Plugins by the wt_ package prefix; review the rest) |
A third-party web half reaches its own Python half through rpc only. HTTP routes from ctx.http.add_routes serve other clients; resources refuses route refs from third-party Plugins. | must | kernelServices.ts |
A third-party Python half registers RPC with ctx.rpc.register and ctx.rpc.topic, never with the module-level rpc / topic decorators, which register untracked core methods. | must | review; a reload then fails with "duplicate rpc method" |
A proposed web service is used only by system Plugins, or by a local-path Plugin that lists it in enabledApiProposals. | must | the window does not hand proposed services to anyone else |
A new stable Python name in api/stable.py needs the owner's sign-off. | must | tests/snapshots/stable_api.json |
Behaviour#
| Rule | Level | Enforced by |
|---|---|---|
Register everything through ctx (Python) or host (web), so unloading undoes it. Do not keep module-level registrations or threads that outlive deactivate. | must | review; the loader disposes only what ctx tracked |
activate(ctx) returns quickly. Long work goes to ctx.on_startup, a Run or a solver child. | should | review |
A Plugin that can be broken by its environment (a missing driver, a missing file) defines health_check(ctx) and returns {"ok": False, "message": ...}, so it fails with a reason instead of half-working. | should | the loader fails the activation on False or ok: False |
A disposable that ctx hands back without tracking it (ctx.fs.watch) is passed to ctx.track(...). | must | review |
| Heavy computation runs in a solver child on the Project interpreter, not in the ProjectServer. | must | review |
An RPC handler takes (rpc_ctx, params) with a pydantic params model, returns a pydantic model or None, and declares the permission scope it needs. | must | app/rpc/registry.py spec_from_function |
Report expected failures with RpcError(code, message). Codes: invalid_params, not_found, conflict, stale_epoch, scene_locked, permission_denied, cancelled, timeout, disconnected, client_unavailable, agent_busy, internal. Any other exception reaches the caller as internal. | must | app/rpc/models.py ErrorCode |
Mark an RPC method idempotent=True only when calling it twice does no harm; only those are retried after a reconnect. | must | review |
Edit the Scene through Tools or ops (an RPC method with lock="scene"), so undo, locks and the op stream see every change. | must | review |
WebSocket traffic is RPC methods and topics only. Add no flat {"type": ...} message. | must | the server answers flat frames with flat_message_retired |
Read and write Project files only through ctx.fs (Python) or resources (web). Never write into .witwin/; Plugin-private state goes to ctx.storage (per Project) or ctx.global_state (per user). | must | ctx.fs refuses paths that leave the Project and paths into .witwin/ |
| Never write runtime data into the Plugin's own source folder. | must | review |
A Project stays usable without Studio: a new file format goes into witwin.studio (layer A), never into Studio-only state. | must | owner rule (decision D11) |
Log with get_logger("<Name>") from witwin_server.api. Never print(). | must | review |
| Put no numpy or other non-JSON types into an RPC result or event. | must | pydantic validation at the boundary |
Interface (web halves)#
| Rule | Level | Enforced by |
|---|---|---|
Build UI from @witwin/ui controls (Button, HeaderButton, IconButton, Input, Select, Dialog, Menu, Tooltip, PanelHeader, Table ...). Never a raw <button>, <input> or <select>, and never hand-made dialog or menu chrome. | must | studio/scripts/check-ui-primitives-adoption.mjs for system Plugins; review for third-party ones |
Use theme tokens for every color (--gray-1..5, --accent and the tokens derived from them). A color literal is allowed only for 3D, canvas, shader or scientific data color, tagged /* allow-color: <reason> */. | must | studio/scripts/check-no-hardcoded-colors.mjs for system Plugins |
Hover hints use Tooltip (or the tooltip prop). Never the native title= attribute. | must | review |
A panel header is PanelHeader or <Toolbar header>; never set a header height by hand. | must | review |
Use lucide-react icons when one exists. | should | review |
A web half works the same on desktop and in the browser, or declares hosts = ["desktop"] in witwin.toml. | must | review; the dual-mode e2e for system Plugins |
Security and trust#
| Rule | Level | Enforced by |
|---|---|---|
Declare in [permissions] everything the Plugin does: Tool tiers, network hosts, processes, files outside the Project, inline shaders. The declaration is shown to the user before trust. | must | unknown keys or tiers make the manifest invalid; tools and tool_scopes are enforced when a Tool registers; shader gates inline GLSL; the rest is review |
List in requestedCapabilities every capability a Tool declares in required_capabilities. | must | the Tool fails to register otherwise (plugins/permissions.py) |
| Never ship secrets, tokens or keys in a Plugin. Credentials come from the user at run time. | must | review |
A web half meant for the browser is signed by the release process (witwin-signature.json). | must | the web window refuses unsigned modules |
A Tool that changes files, runs code or deletes data uses the matching tier (file_write, execute, destructive) so the Agent asks for confirmation, and sets a timeout that fits the work. | must | tools/invoker.py; a warning is logged when an execute or destructive Tool keeps the 30 s default |
Tests and docs#
| Rule | Level | Enforced by |
|---|---|---|
Every Plugin repo keeps tests/test_plugin_api_boundary.py identical to server/scripts/plugin_api_boundary.py. | must | the test itself |
The Python half has pytest tests through witwin_server.api.testing; the web half has a Jest test of activate. | should | review (Testing a Plugin) |
| A system Plugin passes the dual-mode e2e (desktop and web). | must | release gates |
A Plugin that registers Tools gives the Agent a [agent] summary and, for non-trivial Tools, a guide or a skill. | should | review |
Changing @witwin/host, a slot, a witwin.toml key or a ctx member updates these pages and regenerates the reference pages. | must | npm run check:plugin-docs, server/tests/test_plugin_docs.py |
Before you publish#
pytestpasses in the Plugin repo, including the boundary test.witwin.tomlandpyproject.tomlagree on version and requirements;engines.witwinandsdkVersionare set.- The web half loads in Studio on the hosts you declare, with no console errors.
- Templates are rebuilt with
node studio/scripts/build-template-manifest.mjsand committed withtemplates/** -text. [permissions]lists everything the Plugin does.- The release is tagged
v<version>; Publishing, installing and trust has the rest.