Plugin Manifest and Packaging

Define a manifest v2 plugin JAR and distribute it as the release file.

Every Turboism plugin JAR contains exactly one descriptor at:

META-INF/turboism/plugin.json

Runtime accepts schema version 2 only. Version 1 is not loaded or upgraded automatically.

Minimal manifest

{
  "format": "turboism.plugin.meta",
  "schemaVersion": 2,
  "id": "dev.example.plugin.hello",
  "name": "Hello Plugin",
  "version": "0.1.0",
  "description": "Minimal Turboism plugin lifecycle example.",
  "entrypoints": [
    "dev.example.plugin.HelloPlugin"
  ],
  "turboismApi": "[0.1.0,0.2.0)",
  "authors": [
    { "name": "Example Author" }
  ],
  "license": "MIT",
  "website": "https://example.org/hello-plugin",
  "resources": [],
  "i18n": {
    "baseName": "META-INF/turboism/i18n/messages",
    "locales": []
  },
  "dependencies": [],
  "permissions": [],
  "capabilities": [],
  "environment": {
    "requiresCubism": false,
    "ui": "none"
  }
}

Entrypoint rules

Each entrypoint must:

  • exist in the same plugin JAR;
  • be public;
  • implement TurboismPlugin or a subinterface;
  • expose a public no-argument constructor.

Multiple entrypoints share one plugin identity, descriptor, ClassLoader, PluginContext, DisposableScope, permission set, configuration namespace, storage namespace, resources, and localization declaration.

construct/init/enable: manifest order
disable/shutdown:       reverse manifest order

A failure during construction, initialization, or enablement rolls back the entire JAR.

Dependencies

{
  "id": "dev.example.plugin.base",
  "version": "[1.0.0,2.0.0)",
  "type": "required",
  "ordering": "after",
  "reason": "Uses the base plugin contract."
}
  • type is required or optional.
  • ordering is none, before, or after.
  • dependency IDs use reverse-domain syntax.
  • version ranges use the current Turboism version-range grammar.

A dependency record controls resolution and ordering. It does not make another plugin's private implementation package a public Java dependency.

Permissions

Each permission record contains a known ID, application or user scope, and a non-empty reason.

{
  "id": "turboism.action.register",
  "scope": "application",
  "reason": "Registers the Hello action."
}

Declare only permissions required by the SDK services the plugin calls. Permission does not guarantee Provider availability or bypass operation validation.

Resources and localization

Resource roots are normalized relative prefixes ending in /:

"resources": ["icons/"],
"i18n": {
  "baseName": "META-INF/turboism/i18n/messages",
  "locales": ["base", "en", "zh_Hans"]
}

The JAR must then contain:

icons/hello.png
META-INF/turboism/i18n/messages.properties
META-INF/turboism/i18n/messages_en.properties
META-INF/turboism/i18n/messages_zh_Hans.properties

Declared roots and catalogs must exist. Ordinary non-class resources must belong to a declared root. Path traversal, absolute paths, backslashes, empty segments, and undeclared resources are rejected.

Build and validate

For an in-repository plugin module:

./gradlew :plugins:hello:test :plugins:hello:jar validatePluginMeta \
  --no-daemon --console=plain

The repository uses Java 17 and writes module build results under:

build/worktree/<worktreeId>/<module>/libs/

Strict JAR validation

The JAR itself is the installation file. Runtime treats a selected JAR as untrusted local input and validates:

  • a regular file with no symbolic link in the source chain, capped at 16 MiB;
  • exactly one META-INF/turboism/plugin.json;
  • descriptor ID, version, and API-range consistency;
  • no copied SDK, Runtime, test, test-framework, or Live2D classes;
  • no native binaries or installer payloads;
  • no nested JARs;
  • declared entrypoints, resource roots, and i18n catalogs present;
  • no undeclared ordinary resources.

Managed installation stages the validated JAR and applies it after Cubism restarts. Do not create a plugin by renaming an arbitrary ZIP or JAR. Always distribute the JAR produced by the Gradle build.