The manifest

The witwin.toml manifest: id, version, dependencies on other Plugins, API proposals, permissions and what a Plugin contributes.

Studio reads witwin.toml before it runs any Plugin code. It says who the Plugin is, what it needs and what it adds. Keys are camelCase where the loader reads camelCase; this page uses the spelling the loader reads. The generated list of keys the server reads today is witwin.toml keys.

A complete example#

id = "witwin.example_notes"          # identity; also the RPC and Tool namespace
name = "Example Notes"
version = "0.1.0"
description = "Shows the Project's notes.txt as a Console channel."
icon = "notebook"                     # a lucide icon name
author = "WiTwin"
license = "MIT"
repository = "https://github.com/witwin-ai/wt_example"
dependencies = []                     # pip requirements; must match pyproject.toml
# Proposed web services this Plugin uses while in development (source "local").
# A top-level key: in TOML it must come before the first [table] header.
enabledApiProposals = ["slots"]
 
# Plugins this one needs. A required one must be active first; an optional
# one is used when present, and entries in its slots wait while it is off.
[[pluginDependencies]]
id = "studio.console"
version = "^1"
optional = true
 
[engines]
witwin = "^2.0.0"                     # Python Plugin API range
 
[web]
root = "web"                          # folder of the web half
 
[contributes.viewport.host]
entry = "viewport.js"                 # the web half module, relative to [web].root
sdkVersion = "2.0.0"                  # @witwin/host it was built against
 
[permissions]
frontend = true                       # runs code in the Studio window
project_files = "read"
 
[agent]
summary = "Shows the Project's notes.txt in the Console."
guide = "AGENT.md"                    # a file inside the Plugin root

Identity#

KeyMeaning
idThe Plugin's identity: dotted segments of lowercase letters, digits and _, such as witwin.radar or acme.fdtd. RPC refuses any other spelling, so a Plugin with capitals or - cannot register methods. RPC methods, Tools, commands, panel ids and chat-link kinds of a third-party Plugin live under <id>.. The studio. prefix belongs to system Plugins: a third-party Plugin whose id equals a system Plugin's id is ignored (plugins/loader.py load_from_candidates). The folder name does not matter.
name, description, icon, author, license, homepage, repositoryShown on the Plugins page. icon is a lucide name; the default is puzzle.
versionSemver of this Plugin. Other Plugins pin it with pluginDependencies[].version.
enabledfalse ships the Plugin switched off. The user's choice is stored per Project in .witwin/plugins/state.json, not here; for system Plugins too (Disabling, replacing and profiles).
mainThe Python import name, when it differs from the folder name. Without it the folder name is used. The witwin.plugins entry point gives the import name only for a Plugin found in the Project environment (source = "environment").

Python requirements and the Plugin API#

KeyMeaning
dependenciespip requirements of the Python half. setup.py fails the build when pyproject.toml lacks one. Studio installs them only for cloud-installed Plugins; a local checkout or an environment Plugin reports Missing dependencies: [...] instead.
engineswitwin = "^2.0.0": the range of the Python Plugin API (WITWIN_API_VERSION, server/witwin_server/__init__.py, 2.0.0 since D16). Outside the range the Plugin does not load, so a manifest that still says ^1.0.0 is refused. Without the key no check is made. Ranges use the semver subset in Publishing, installing and trust.
activationEventsLoad the Python half lazily, on the first matching event: onView:<panelId>, onCommand:<commandId>, onScene:<token>, workspaceContains:<glob> (a file in the Project matches), onStartupFinished, or * (after the server has started). Without the key the Python half is imported at load.
requiresRestarttrue: installing this Plugin takes effect at the next ProjectServer start.
isolationin_process (default) or out_of_process. The second is a request only: until the PluginHost ships, such Plugins also run in-process after trust.

Dependencies between Plugins#

[[pluginDependencies]]
id = "studio.project"     # required: without it this Plugin does not activate
version = "^1"
 
[[pluginDependencies]]
id = "studio.properties"  # optional: activate anyway; its slot entries wait
version = "^1"
optional = true

An inline array works too: pluginDependencies = [{ id = "studio.em" }].

FieldMeaning
idThe other Plugin's id. System Plugins are studio.<name>.
versionSemver range; * when left out.
optionalfalse (default): the server blocks this Plugin while the other is missing, disabled or blocked, and unloads it with the other. Enabling only the other later does not bring it back; enabling this Plugin loads its required dependencies first. true: this Plugin activates without it; what it registers into the other Plugin's slots waits and attaches when that Plugin is enabled. A present dependency outside version blocks the Plugin even when optional = true.
activationRecorded only; it changes nothing today.
capabilitiesCapability ids this dependency must provide. Each becomes a capability import with the dependency's version and optional (see Capabilities below).

Both halves use the same list. The server orders Python activation by it and blocks every Plugin in a cycle (plugins/resolver.py). The window orders web halves by it, optional dependencies included (studio/src/app/pluginHost/pluginRuntime.ts); it ignores version ranges and keeps the input order inside a cycle. The C layer of the Python API (Python API) lets you import witwin_server.plugins_builtin.<name>.api only for a studio.<name> listed here, and another Plugin's api module only when that Plugin is listed.

A system Plugin's web half lists its dependencies in definePlugin({ dependencies: [...] }), because it has no witwin.toml of its own in the window.

Capabilities#

A capability is a live object (a registry, a service) shared without an import, so the consumer is told when the provider unloads.

[[contributes.capabilities]]
id = "witwin.motion_matching.body_collision"
version = "1.0.0"
module = "witwin_motion_matching.collision"   # optional, shown only
factory = "make_provider"                     # optional, shown only
 
[[imports.capabilities]]
id = "witwin.motion_matching.body_collision"
version = "^1"
optional = true

contributes.capabilities declares what this Plugin provides; module and factory are shown on the Plugins page and are never called. The provider still calls ctx.capabilities.provide(...) in activate; the declaration alone provides nothing.

imports.capabilities lists capabilities this Plugin needs. A required capability import keeps the Plugin imported but not activated (state waiting_capabilities) until a provider arrives. ctx.capabilities.require(...) works only for a capability declared here or through a dependency's capabilities; any other id raises CapabilityAccessError.

The loader reads only the camelCase spellings: requires_restart and requested_capabilities are ignored.

requestedCapabilities is a separate, older list of host capability names a Plugin's Tools need (plugins/permissions.py). Most Plugins leave it out, but a Plugin whose Tool declares required_capabilities must list each of them here, or registering that Tool fails.

The web half#

KeyMeaning
[web].rootFolder that holds the web half and its static files, relative to the Plugin root. Default web.
[contributes.viewport.host].entryThe ES module the window imports. It is looked up under [web].root first, then under the Plugin root. It must export default definePlugin({...}) (SDK 2).
[contributes.viewport.host].sdkVersionThe @witwin/host version the module was built against (sdk is read too). Without it the version counts as 1.0.0 and the module is refused, so always write it. The window refuses a different major, and refuses SDK 1 modules (export function activate). The module may also export sdkVersion; both are checked.
enabledApiProposalsProposed services (@witwin/host/proposed) this Plugin may inject, such as ["slots", "fs"]. They are handed over only while the Plugin is loaded from a local path (development). A system Plugin needs no list. See Web API.

A web half runs code in the editor. An installed Plugin's web half loads only after the user grants "Trust frontend code", and Studio offers that grant only when the manifest says permissions.frontend = true. A local or system web half loads without the grant. Studio on the web also needs an official signature (Publishing, installing and trust).

Permissions (a disclosure, not a sandbox)#

[permissions] is what the trust dialog shows. It does not limit the code: a trusted Plugin runs in-process with the ProjectServer's rights. Three keys are enforced anyway: tools and tool_scopes when a Tool registers (see the Agent section of python-api.md), and shader through a separate grant. Other hard limits are elsewhere: the per-call Tool tier gate, Project path checks, and the hosted refusal table. An unknown key makes the manifest invalid, and the Plugin does not load.

KeyTypeMeaning
toolslist of tiersAgent Tool tiers it registers: read, soft_write, scene_write, file_write, execute, destructive. A Tool whose tier is above the highest tier listed here fails to register. A Plugin with Tools (here or in contributes.tools) asks for consent when the user enables it; an installed or environment Plugin always asks.
tool_scopeslistExtra Tool scopes.
scene, project_filesbool, string or listWhat it reads or writes.
networklist of hostsHosts the Python half contacts, such as ["overpass-api.de"]. Shown in the trust dialog; nothing opens or blocks traffic by it.
secretslistSecret names it reads.
subprocess, filesystem, skills, frontend, shaderboolStarts processes, reads outside the Project, ships Agent skills, runs code in the window, uses inline GLSL. frontend makes the "Trust frontend code" grant available; shader the inline-GLSL grant (Publishing, installing and trust).
dependencieslistFilled from dependencies when left out.

What the Agent reads: agent#

[agent]
summary = "FDTD solver with sources, monitors and field plots."
guide = "AGENT.md"

summary is the Plugin's line in every Session's plugin index (default: the first sentence of description), cut to 160 characters. guide is a Markdown file inside the Plugin root that plugin.describe returns, up to 12,000 characters: when to use the Plugin, a typical setup, units, pitfalls. More on the Agent pieces: Python API.

contributes.*: declarations that need no code#

Most of these register when the manifest loads, before the Python half runs: webview panels, view definitions, command stubs, menus, keybindings, configuration, viewport passes, colors, themes, solvers, models and entity types (plugins/contribution_stubs.py). A command stub activates a lazy Plugin when it runs. The others come later: tools and the view providers register when the Python half activates, and skills and agents are read by studio.agent from the loaded Plugins each time it needs them. templates are read by witwin.studio, not by the loader. The code path for the same thing is in Python API or Web API; use one, not both.

KeyShapeWhat it adds
contributes.tools[{module, register?, namespaces?}]Agent Tools: imports <module> (relative to the Plugin package) at activation and calls register(ctx) (register is the default function name). Names become <id>.<name> unless the Plugin keeps a namespaces prefix it took over from core. A namespace has at least two lowercase segments (scene.edit) and belongs to one Plugin only.
contributes.skills[{root}]SKILL.md folders for the Agent (default skills/).
contributes.agents[{id, label?, description?, mode?, readonly?, model?, effort?, tools?, deny_tools?, max_steps?, skills?, prompt or prompt_file}]Agent profiles. id matches ^[a-z0-9][a-z0-9_.-]{0,63}$; mode is subagent (default), primary or all; model is provider/model, a model name or inherit; prompt_file stays inside the Plugin root; the prompt is at most 16,000 characters. An invalid entry is skipped with a log line; the first Plugin to use an id keeps it.
contributes.models[{id, path, title, category, tags}]Parametric model files; each becomes a Library item and a geometry generator (dev_docs/backend/SCRIPT_GEOMETRY.md).
contributes.solvers[{...}]Out-of-process solvers (RFC-012): id, entry, runtime, and the hardware they need (resources, below). Fields below.
contributes.templates[[contributes.templates]]Project templates: id, title, icon, version, dir, manifest, sha256, preview (below).
contributes.entityTypes[{...}]Scene entity kinds without Python code (RFC-008).
contributes.commands[{id, title, category, icon, when?}]Command palette entries; running one activates a lazy Plugin.
contributes.menus{ "<surface>": [{command, when?, group?}] }Menu rows on a surface such as viewport/toolbar.
contributes.keybindings[{key, command, when?, args?}]Shortcuts such as ctrl+alt+f.
contributes.configuration{title?, properties: {key: {type, default, ...}}}A Settings page built from the declaration.
contributes.colors[{id, defaults: {dark, light}}]Theme tokens. id is the whole CSS variable name; no prefix is added. light defaults to dark.
contributes.themes[{id, name, mode, path or tokens}]Whole themes.
contributes.webviewPanels[{id, title, icon, menuPath, entry}]An iframe panel with a Python WebviewController.
contributes.views[{id, ...}]Tree views served by a Python provider.
contributes.viewport{host, passes}host: the web half (above). passes: built-in viewport passes to switch on.
contributes.capabilities[{id, version, module?, factory?}]Capabilities this Plugin provides (above).
contributes.rpc{domains, modules}System Plugins only. Core RPC domains (git for git.*) and the modules whose @rpc methods the Plugin owns. A third-party Plugin always registers under <id>.* with ctx.rpc.register.
contributes.kernel{handlers, startup, activate}System Plugins only. Feature code that stays in witwin_server.features because the kernel imports it too, registered by name ("module:attr") so the Plugin package imports only witwin_server.api: activate are functions called with the Plugin's ctx when it activates, handlers are WebSocket Handler classes, startup are startup steps ({ fn, late = true } for a late one). Read by plugins/builtin.py _apply_kernel_contribution.

when is a context-key expression (&& || ! == != in ( )) over keys such as sceneHasSelection, connected, platform.sameMachine.

Solvers: [[contributes.solvers]]#

A solver is a child process that Studio starts on demand and keeps warm (sdk/src/witwin/studio/solver/spec.py). An invalid entry fails the whole Plugin load.

KeyMeaning
idWrite it as <plugin id>.<name>: ctx.solvers prefixes a short name with the Plugin id.
entryThe solver script, relative to the Plugin root. It must exist and stay inside the root.
runtimeRequired table with one of python, conda (an environment name) or command, plus optional args and env. python = "${project:python}" is the Project interpreter; ${backend:python} is the ProjectServer's own; ${NAME} is an environment variable and fails when it is not set. A relative python path is resolved from the Plugin root.
title, versionShown in the solver list; version defaults to the Plugin's.
lifecycle, maxParallelOnly warm and 1 are accepted today.
idleTimeoutSecSeconds before an idle child stops (300).
cancelGraceSecSeconds a cancelled child gets before it is killed (5).
heartbeatIntervalSec, heartbeatTimeoutSec, heartbeatMissesLiveness checks (30, 5, 2).
capabilitiesA free table the solver reports to callers.
resourcesThe hardware it needs (next section).

Solver hardware: [contributes.solvers.resources]#

A solver that needs a GPU says so, and Studio checks this machine before it starts the solver's child (remote-solver plan R0, dev_docs/remote-solver-plan.md):

[[contributes.solvers]]
id = "witwin.maxwell.fdtd"
entry = "solver_host.py"
 
[contributes.solvers.runtime]
python = "${project:python}"
 
[contributes.solvers.resources]
gpu = 1                        # NVIDIA cards; 0 or no table: a CPU is enough
min_vram_gb = 16               # memory of each card, in GB as the card is sold
cuda_arch = ["sm_86", "sm_89"] # optional: the architectures the Plugin is compiled for
remote = true                  # optional, default true; false: never run elsewhere
KeyTypeMeaning
gpuwhole number >= 0How many NVIDIA cards one solve needs.
min_vram_gbnumber >= 0Memory each of those cards must have.
cuda_archlist of sm_NNWhen not empty, each card must be one of these.
remoteboolfalse keeps the solver on this machine whatever placement says.

Unknown keys are an error, so a misspelt minVramGb does not silently mean "any card". When no card of this machine fits, the solve fails before its child starts with error code solver_needs_gpu and a message that names what the solver needs and what the machine has. Cards are read with nvidia-smi --query-gpu; a driver that reports no memory or architecture counts as fitting.

Project templates: [[contributes.templates]]#

A template is a folder that Studio copies, as it is, into a new Project. It appears as a card in the template grid and in the New Project dialog.

[[contributes.templates]]
id = "radar-sim"            # a Studio catalog id fills that card; a new id adds a card
title = "Radar Sim"
icon = "radar"              # lucide icon name
version = "1.0.0"           # must equal the content manifest's templateVersion
dir = "templates/radar-sim" # relative to the Plugin root
manifest = "template.manifest.json"
sha256 = "<sha256 of the manifest file>"
preview = "templates/radar-sim.webp" # optional card image, 1600x1000, relative to the Plugin root

The content manifest lists every file with its byte size and sha256, plus schemaVersion (1), templateId, templateVersion and treeSha256. Build it, and print the sha256 to paste above, with:

node studio/scripts/build-template-manifest.mjs <plugin>/templates/<id>

Rules, checked by witwin.studio (sdk/src/witwin/studio/_templates.py), the only code that reads, verifies and copies templates:

  • The folder must contain witwin.toml. A template is a complete Python Project: its pyproject.toml depends on the Plugin, pinned the way witwin add writes it, and its scripts/ and tests/ run with witwin.studio alone.
  • id matches ^[a-z0-9][a-z0-9.-]{0,63}$.
  • Allowed files: .wtscene, .wtimport, .glb, .toml, .json, .md, .txt, .py, .gitignore and .python-version. A path part named .witwin, results, plugins, .venv or __pycache__ is refused in the content manifest. On disk, a __pycache__ folder that pip wrote next to an installed template is skipped and not copied; the other runtime folders still stop the copy. Symlinks and junctions are refused.
  • Limits: 1000 files, 8 MiB per file, 32 MiB in total. A preview larger than 4 MiB, outside the Plugin root, or not .webp, .png, .jpg or .jpeg is ignored, and the card shows its icon instead.
  • Paths must work on Windows: forward slashes only, relative, no empty, . or .. part, none of <>:"|?*, no part that ends in a dot or a space, and no reserved name (con, prn, aux, nul, com1-com9, lpt1-lpt9, with or without an extension).
  • Any file whose size or hash differs from the manifest stops the copy. Commit template folders with line-ending conversion off (templates/** -text in the Plugin's .gitattributes); otherwise a Windows checkout turns LF into CRLF and every hash fails.
  • A card whose Plugin is not installed shows a download mark and opens the Plugins page. A card whose content is missing is muted.

How Studio finds templates:

  • Desktop. Electron runs witwin template list --json and witwin template create --json on the backend interpreter (studio/electron/project/templates.ts). It passes the roots of the Plugins this machine has: WITWIN_LOCAL_PLUGIN_PATHS, WITWIN_PLUGINS_DIR, the global local_paths.json and the global plugin store's packages/ (studio/electron/config.ts listInstalledPluginRoots). Templates of a Plugin found only in a Project's environment do not appear.
  • Web. A cloud Project is created blank. Once its sandbox is open, the RPC project.template.apply fills it from a template of a Plugin that is preinstalled in the sandbox image (server/machine-plugin-sources.lock). The web shows only the cards in HOSTED_TEMPLATE_PLUGINS (studio/src/features/project/projectTemplates.ts), so a new template reaches the web only after both lists name its Plugin.

Composition: replaces and hosts#

KeyMeaning
replaces = ["studio.console"]This Plugin takes the place of another while it is active; "<slot>:<key>" takes one keyed slot entry instead. A single string works too. Only a trusted Plugin may; a Plugin cannot replace itself, and the kernel and the security core cannot be replaced.
hosts = ["desktop", "web"]Where the Plugin may load: desktop, web, headless. Without it, everywhere. Elsewhere it does not load and the Plugins page says why. Any other value makes the manifest invalid: the Plugin is skipped, with only a log line.

Which system Plugins a host mode loads (profiles) and how a Project turns one off: Disabling, replacing and profiles.

System Plugins#

A system Plugin (source = "builtin") has two halves in this repository: server/witwin_server/plugins_builtin/<dir>/witwin.toml with its Python half, and studio/src/plugins/<dir>/index.ts (or index.tsx) with its web half, listed in studio/src/plugins/index.ts. Their manifests use the same keys. Only system Plugins may use contributes.rpc, claim a core RPC domain, add Handler classes or mount routes at the root.