Publishing, installing and trust
Where Studio finds Plugins, how a Project installs one with witwin add, what a release needs, how trust works and how versions resolve.
Where Studio finds Plugins#
System Plugins (source = "builtin") ship inside Studio and are not
part of the search below: plugins/builtin.py reads them from
server/witwin_server/plugins_builtin/. They are trusted in advance, cannot
be uninstalled and can be disabled. Their ids cannot be taken over: a
third-party Plugin with a system Plugin's id is ignored.
Third-party Plugins, highest precedence first (discover_plugin_roots in
server/witwin_server/plugins/paths.py):
- Local paths (
source = "local"), read in this order:WITWIN_LOCAL_PLUGIN_PATHS(the Plugins panel and--plugin-pathfeed it),WITWIN_PLUGINS_DIR, the Project's.witwin/plugins/local_paths.json, then the global~/.witwin/plugins/local_paths.json(orWITWIN_GLOBAL_PLUGINS_DIR). A path may be a Plugin root or a folder whose direct subfolders are Plugin roots. This is development: the Plugin may use the proposed web services in itsenabledApiProposals, and Studio never runs pip for it. - The Project environment (
source = "environment"):witwin.pluginsentry points installed in the Project's.venv(orWITWIN_PROJECT_PYTHON) byuv sync, only for a Project withpyproject.toml. Read statically; trusted once per Project. - Cloud and system packages recorded in
.witwin/plugins/lock.json(rows of typecloudorsystemonly; a row cannot name a local path). - Project-vendored packages under
.witwin/plugins/installed.
When two candidates have the same id, Studio keeps one version per id. It
picks among the candidates that satisfy every required version range of
the Plugins that depend on it; precedence breaks the tie only among those.
Nothing is scanned implicitly: not the current folder, not sibling folders.
A hosted ProjectServer (a cloud sandbox, core/hosted.py) loads only what
the operator installed. It skips the Project's local_paths.json, the
Project environment and Project-vendored packages, and takes a lock.json
row only when its package is already in the operator's plugin store.
Install sources#
Plugins never come from PyPI. A Project installs one with witwin add
(the Plugins page runs it), which writes pyproject.toml and
[tool.uv.sources]:
| Source | Example |
|---|---|
| a local folder | witwin add ../wt_notes |
| a git URL | witwin add git+https://github.com/acme/wt_notes@v0.2.0 |
| the witwin index | witwin add wt-notes (a PEP 503 tree on GitHub Pages) |
Every first-party name (wt-*, witwin-studio) is pinned to the witwin
index so uv cannot resolve it from PyPI. Details: section 3.6 of
dev_docs/project-as-python-codebase.md.
What a release needs#
witwin.tomlversionbumped, and the same version in the distribution (setup.pyreads it fromwitwin.toml).- Every
dependenciesentry also inpyproject.toml; the build fails otherwise. - The web half built against
@witwin/host2.x, with[contributes.viewport.host] sdkVersion = "2.0.0". Studio refuses an SDK 1 module. - No
enabledApiProposalsneeded at run time: an installed release cannot use proposed services. If it does, the panel fails on every installed machine. Move to stable services or keep the Plugin as a local path until the service is stable. - Templates rebuilt with
node studio/scripts/build-template-manifest.mjsand committed withtemplates/** -text. - The boundary check and the Plugin's tests green.
Trust#
Trust applies to installed Plugins (from the cloud, a git URL or a zip) and
to environment Plugins. Local-path and system Plugins run without it
(plugins/trust.py evaluate_plugin_trust).
An installed Plugin that runs code is inert until the user trusts it.
Installing shows the permission disclosure from [permissions]; granting it
is stored per Project in .witwin/plugin-consent.json and enables the
Plugin. A trusted Plugin runs in-process with the ProjectServer's rights.
The manifest is a disclosure, not a sandbox.
Environment Plugins arrive with a git pull, so they also wait for trust;
Studio asks once per Project and names every Plugin it trusts.
Any Plugin with Agent Tools, whatever its source, asks for consent when the
user enables it (requires_consent in plugins/loader.py).
A Plugin counts as declarative, and installs and runs without the trust
step, only when all of these hold (plugins/trust.py
_has_executable_code):
- no
mainand noactivationEvents; contributeshas onlycolors,themes,menus,keybindings,configuration,skillsandentityTypes, and no entity type names amodule;- no
*.pyfile anywhere in the Plugin folder.
Two more grants cover the window, also stored in plugin-consent.json and
needed only by installed Plugins:
- Trust frontend code (
plugin.frontend_consent.grant): lets the web half load. Studio offers it only whenpermissions.frontend = true. - Shader (
plugin.shader_consent.grant): lets the Plugin push inline GLSL to the viewport. Studio offers it whenpermissions.shader = true; frontend trust includes it.
Publisher verification for the Python consent prompt is not built yet:
every runtime-installed third-party Plugin shows "Unverified publisher"
(server/witwin_server/api/signing.py). Web halves on Studio Web are
checked separately, by the signature below.
Web halves in the browser: official signatures#
Studio on the web loads a Plugin's web half only when the module carries a valid official signature. The desktop app does not check signatures; it relies on the trust step above.
- The release process writes
witwin-signature.jsoninto the Plugin root (schema = "witwin.plugin.signature.v1",plugin_id,version,key_id, a sha256 for each listed file, and an Ed25519signature).fileslists at least the[contributes.viewport.host] entry. - The window fetches the signature, verifies it against the public keys
built into Studio (
studio/src/features/viewport/host/officialKeys.ts), fetches the module, compares its sha256, and imports exactly those bytes. - If any step fails, the web half does not load and the user sees "Open this Plugin in the desktop app". The Python half, settings and webviews keep working.
- Sign with
node studio/scripts/sign-plugin.mjs <plugin dir> --key <pem>. The private key lives only on the release machine or in CI secrets.
Format details: section 10 of dev_docs/HOST_CONTRACTS.md.
Versions#
pluginDependencies[].versionis a semver range against the other Plugin'sversion. A dependency outside the range keeps your Plugin blocked, with the reason on the Plugins page. This holds for an optional dependency too: optional means "may be absent", not "any version".engines.witwinis the range of the Python Plugin API (WITWIN_API_VERSION).- The selected version of a cloud package is per Project, in
.witwin/plugins/lock.json.
Ranges use a small semver subset (server/witwin_server/utils/semver.py):
| Form | Meaning |
|---|---|
*, x or empty | Any version. |
1.2.3 or =1.2.3 | Exactly that version. |
^1.2.3 | Same major: >=1.2.3 <2.0.0. There is no special case for ^0.x: ^0.2.0 also accepts 0.9.0. |
~1.2.3 | Same major and minor: >=1.2.3 <1.3.0. |
>=, >, <=, < | Comparisons. Several, separated by spaces, must all hold: >=1.2 <2. |
Partial versions work (^1, >=1.2), and a leading v is allowed.
Pre-release tags are ignored when comparing. || is not supported.