NarraLeaf

Manifest

Every field in manifest.json — the entries, the full contributes surface, and why most permissions are derived rather than written.

manifest.json is the only file Studio reads without executing your code. It declares what the plugin provides, and everything the install prompt shows is computed from it.

{
  "manifestVersion": 2,
  "id": "yourname.hello",
  "name": "Hello",
  "version": "1.0.0",
  "publisher": "Your Name",
  "description": "A starter plugin.",
  "entries": { "studio": "main.js", "runtime": "runtime.js" },
  "contributes": {
    "blueprintNodes": ["yourname.hello.log"],
    "runtimeCapabilities": ["store"]
  },
  "permissions": []
}

Top-level fields

FieldRequiredNotes
manifestVersionyesAlways 2. Version 1 is rejected.
idyesNamespaced: publisher.plugin-name, lowercase, at least one dot.
nameyesDisplay name.
versionyesSemver (1.0.0), optionally with a prerelease or build suffix.
publishernoShown in the plugin list.
descriptionnoOne line, shown at install.
entriesyes{ studio?, runtime? } — package-relative ESM paths. At least one; both files must exist.
contributesnoEverything the plugin provides. Omitting a key is the same as an empty list.
permissionsnoAuthor-declared privileged capabilities only — see below.

Entry paths cannot be absolute or contain .., ., null bytes, ?, or #.

contributes

contributes is the single source of truth for what a plugin can do. Studio validates a project against it without running plugin code, and the install permission set is derived from it.

KeyTypeWhat it declares
blueprintNodesstring[]Blueprint node types this plugin provides.
widgetsstring[]Widget element types this plugin provides.
localesobject[]Studio language packs — locales the plugin adds or fills.
runtimeDatastring[]Plugin storage namespaces to publish with the game.
runtimeCapabilitiesstring[]Capability domains the runtime entry may use.
sidecarsobject[]Native child processes shipped inside the author's game.
buildDependenciesobject[]External binaries fetched and cached at build time.

Every node type, widget type, storage namespace, sidecar id, and build-dependency id must be prefixed with your plugin id. An unknown key under contributes is rejected rather than ignored.

blueprintNodes and widgets

Registering a type you did not declare throws at load time, on both entries. The declaration is what lets Studio tell an author before a build that a node their project uses has no runtime provider — instead of shipping a game with a node that does nothing.

runtimeData

Plugin storage lives under the project's editor directory, which is never packaged. If your runtime entry needs data an author authored in Studio — a catalog, a lookup table — list those namespaces here and read them with app.game.data.readJson(namespace).

{ "contributes": { "runtimeData": ["yourname.hello.catalog"] } }

The list is an explicit allowlist so editor-only plugin state cannot leak into a shipped game by accident. readJson is synchronous — the data travels with the pack — and returns null when the namespace was not declared, the project never wrote it, or the game predates the data being published. Degrade gracefully; do not assume authored data exists.

runtimeCapabilities

The nine capability domains, each mapping onto one namespace of app.game. An undeclared domain is absent from the object, not a method that throws. Declaring one without an entries.runtime is a manifest error.

{ "contributes": { "runtimeCapabilities": ["store", "events", "state.read"] } }

store · events · state.read · state.write · saves.read · saves.write · ui.overlay · assets · locale

The full model, what each one grants, and how environments narrow it further: Runtime API.

sidecars and buildDependencies

The two heavy declarations. A sidecar is a native child process that ships inside the games an author builds; a build dependency is an external binary Studio downloads, verifies, and caches at build time. Both are keyed by <platform>-<arch> and both require a sha256.

locales

A Studio language pack — a locale the plugin adds, or gaps it fills in a built-in one. See Make a plugin. A manifest with only contributes.locales and no entry code is a valid plugin.

Permissions are derived

Install permissions come in two families, and the split is the point.

Author-declared — written by you in permissions[]. These are privileged Studio controls that have nothing to do with contributes:

{
  "permissions": [
    { "kind": "filesystem", "path": "/absolute/path", "mode": "readwrite", "recursive": true },
    { "kind": "api", "capability": "bash.execute" }
  ]
}
KindShape
filesystem{ path, mode: "read" | "write" | "readwrite", recursive } — a real path string.
api{ capability } — currently only bash.execute is usable by a plugin.

These affect the studio entry only. Grants are recorded per pluginId@version, so bumping your version requires the author to approve again.

Derived — computed by Studio from contributes, never written by you:

KindDerived from
runtimeEach entry in contributes.runtimeCapabilities.
sidecarEach contributes.sidecars entry, with the platform keys it ships.
buildDependencyEach contributes.buildDependencies entry, with the hostnames it downloads from.

Writing a runtime, sidecar, or buildDependency permission by hand is a manifest error — the plugin fails to install with "permission kind … is derived from contributes and must not be declared by hand." Declare the capability once, in contributes, and the permission follows.

This is what keeps the install prompt honest: a capability is declared in exactly one place, so what the prompt lists and what the plugin can actually reach cannot drift apart. It also makes updates behave — adding a capability widens the derived permission set, which re-prompts the author automatically, while a version that widens nothing inherits the existing grant.

Validation failures

SymptomCheck
Install rejectedmanifest.json is not valid JSON, or id / version / entries failed validation.
"requires a runtime entry"runtimeCapabilities or sidecars declared without entries.runtime.
"must not be declared by hand"A derived permission kind written into permissions[].
"Unknown plugin runtime capability"A typo in runtimeCapabilities. The list is closed.
Registration throws at loadA node or widget type that is not in contributes, or not prefixed with the plugin id.
Preview fails with "Plugin validation failed"A node or widget the project uses has no runtime provider — plugin disabled, missing, no entries.runtime, or the type is not declared.
Digest mismatch on installA sidecar file's bytes do not match its sha256.

On this page