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#

RuleLevelEnforced 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 _.mustthe 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.musta 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.mustwitwin 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.mustapp/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.mustctx.tools, plugins/manifest.py _TOOL_NAMESPACE
Python events (ctx.events.publish) and web events (events.emit) start with <plugin id>..mustplugins/events.py; the web events service throws for a third-party Plugin
Panel ids are <plugin id> or <plugin id>.<name>.mustbuildHost.tsx registerPanel
A Plugin's own witwin:// URI kinds and chat-link kinds are <plugin id>.<name>.mustthe web opener service and registerChatLink refuse other kinds
Solvers are declared as <plugin id>.<name>, so ctx.solvers finds them.mustapi/solver_api.py prefixes ids that lack it
Command ids, setting keys and slot ids start with the Plugin id: witwin.example_notes.reload.shouldreview; not checked
Component names are PascalCase (Material, RadarSensor); component field keys are snake_case (display_name).mustcomponent 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.mustreview
Use the canonical words: Project, Scene, Script, Plugin, Session, Run, Result. Never "workspace", "artifact" or "extension" as domain words.mustreview (dev_docs/CONCEPTS.md)

Versions and compatibility#

RuleLevelEnforced by
version in witwin.toml is SemVer and equals the distribution version.mustthe shared setup.py takes the version from witwin.toml
Declare the Python Plugin API range: [engines] witwin = "^2.0.0".mustplugins/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.mustapp/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.mustthe 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.mustowner 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.mustwitwin.studio _templates.py refuses the template

Code boundaries#

RuleLevelEnforced 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]].mustserver/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.mustreview; 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.mustnpm 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.mustnpm 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.mustthe 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.mustkernelServices.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.mustreview; 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.mustthe window does not hand proposed services to anyone else
A new stable Python name in api/stable.py needs the owner's sign-off.musttests/snapshots/stable_api.json

Behaviour#

RuleLevelEnforced by
Register everything through ctx (Python) or host (web), so unloading undoes it. Do not keep module-level registrations or threads that outlive deactivate.mustreview; the loader disposes only what ctx tracked
activate(ctx) returns quickly. Long work goes to ctx.on_startup, a Run or a solver child.shouldreview
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.shouldthe 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(...).mustreview
Heavy computation runs in a solver child on the Project interpreter, not in the ProjectServer.mustreview
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.mustapp/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.mustapp/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.mustreview
Edit the Scene through Tools or ops (an RPC method with lock="scene"), so undo, locks and the op stream see every change.mustreview
WebSocket traffic is RPC methods and topics only. Add no flat {"type": ...} message.mustthe 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).mustctx.fs refuses paths that leave the Project and paths into .witwin/
Never write runtime data into the Plugin's own source folder.mustreview
A Project stays usable without Studio: a new file format goes into witwin.studio (layer A), never into Studio-only state.mustowner rule (decision D11)
Log with get_logger("<Name>") from witwin_server.api. Never print().mustreview
Put no numpy or other non-JSON types into an RPC result or event.mustpydantic validation at the boundary

Interface (web halves)#

RuleLevelEnforced 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.muststudio/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> */.muststudio/scripts/check-no-hardcoded-colors.mjs for system Plugins
Hover hints use Tooltip (or the tooltip prop). Never the native title= attribute.mustreview
A panel header is PanelHeader or <Toolbar header>; never set a header height by hand.mustreview
Use lucide-react icons when one exists.shouldreview
A web half works the same on desktop and in the browser, or declares hosts = ["desktop"] in witwin.toml.mustreview; the dual-mode e2e for system Plugins

Security and trust#

RuleLevelEnforced 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.mustunknown 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.mustthe 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.mustreview
A web half meant for the browser is signed by the release process (witwin-signature.json).mustthe 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.musttools/invoker.py; a warning is logged when an execute or destructive Tool keeps the 30 s default

Tests and docs#

RuleLevelEnforced by
Every Plugin repo keeps tests/test_plugin_api_boundary.py identical to server/scripts/plugin_api_boundary.py.mustthe test itself
The Python half has pytest tests through witwin_server.api.testing; the web half has a Jest test of activate.shouldreview (Testing a Plugin)
A system Plugin passes the dual-mode e2e (desktop and web).mustrelease gates
A Plugin that registers Tools gives the Agent a [agent] summary and, for non-trivial Tools, a guide or a skill.shouldreview
Changing @witwin/host, a slot, a witwin.toml key or a ctx member updates these pages and regenerates the reference pages.mustnpm run check:plugin-docs, server/tests/test_plugin_docs.py

Before you publish#

  1. pytest passes in the Plugin repo, including the boundary test.
  2. witwin.toml and pyproject.toml agree on version and requirements; engines.witwin and sdkVersion are set.
  3. The web half loads in Studio on the hosts you declare, with no console errors.
  4. Templates are rebuilt with node studio/scripts/build-template-manifest.mjs and committed with templates/** -text.
  5. [permissions] lists everything the Plugin does.
  6. The release is tagged v<version>; Publishing, installing and trust has the rest.