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):

  1. Local paths (source = "local"), read in this order: WITWIN_LOCAL_PLUGIN_PATHS (the Plugins panel and --plugin-path feed it), WITWIN_PLUGINS_DIR, the Project's .witwin/plugins/local_paths.json, then the global ~/.witwin/plugins/local_paths.json (or WITWIN_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 its enabledApiProposals, and Studio never runs pip for it.
  2. The Project environment (source = "environment"): witwin.plugins entry points installed in the Project's .venv (or WITWIN_PROJECT_PYTHON) by uv sync, only for a Project with pyproject.toml. Read statically; trusted once per Project.
  3. Cloud and system packages recorded in .witwin/plugins/lock.json (rows of type cloud or system only; a row cannot name a local path).
  4. 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]:

SourceExample
a local folderwitwin add ../wt_notes
a git URLwitwin add git+https://github.com/acme/wt_notes@v0.2.0
the witwin indexwitwin 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.toml version bumped, and the same version in the distribution (setup.py reads it from witwin.toml).
  • Every dependencies entry also in pyproject.toml; the build fails otherwise.
  • The web half built against @witwin/host 2.x, with [contributes.viewport.host] sdkVersion = "2.0.0". Studio refuses an SDK 1 module.
  • No enabledApiProposals needed 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.mjs and committed with templates/** -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 main and no activationEvents;
  • contributes has only colors, themes, menus, keybindings, configuration, skills and entityTypes, and no entity type names a module;
  • no *.py file 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 when permissions.frontend = true.
  • Shader (plugin.shader_consent.grant): lets the Plugin push inline GLSL to the viewport. Studio offers it when permissions.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.json into the Plugin root (schema = "witwin.plugin.signature.v1", plugin_id, version, key_id, a sha256 for each listed file, and an Ed25519 signature). files lists 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[].version is a semver range against the other Plugin's version. 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.witwin is 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):

FormMeaning
*, x or emptyAny version.
1.2.3 or =1.2.3Exactly that version.
^1.2.3Same 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.3Same 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.