Skip to content

SPI and manifest

A plugin JAR is not just code with a ServiceLoader entry. It carries three independent, bounded contracts, and an Ed25519 envelope that binds them.

shipment-yano-plugin.jar
└── META-INF/yano/
├── plugins/plugin-bundle.shipment.json runtime contributions
├── appchain-config-metadata-v1.json typed configuration (optional)
├── appchain-component-catalog-v1.json selectable product capabilities
└── appchain-component-catalog-v1.sig.json Ed25519 trust envelope
FileOwns
plugins/<bundle-id>.jsonThe executable runtime contributions — what this bundle actually provides to a running node, and the Yano API major and min/max levels it is compatible with.
appchain-config-metadata-v1.jsonTyped configuration definitions: keys, types, defaults, allowed values, scope, change policy, and whether a value is secret.
appchain-component-catalog-v1.jsonSelectable product capabilities and the artifacts they require — what a user sees in appchain capabilities and in Studio.

They are deliberately separate. A tool can read the catalog to offer a capability without loading any code, and a node can validate the runtime manifest without interpreting product metadata.

Runtime activation goes through PluginProviderRegistry plus the schema-v1 plugin manifest. There is no other path:

  • no raw ServiceLoader discovery by the host,
  • no direct host construction,
  • no product switches inside the host, and
  • no product-specific host CDI or REST activation.

The host loads a bundle from yano.plugins.directory, validates its manifest, resolves declared dependencies, checks API compatibility, and only then activates the contributions that configuration has selected.

Requirements a runtime plugin must satisfy:

RequirementWhy
Dependency-complete bundleThe node must not have to resolve your transitive dependencies at runtime.
Does not embed host SPI classesEmbedding them creates two incompatible copies of the same interface.
Declares Yano API major and min/max levelA bundle built against an incompatible host fails closed rather than misbehaving.
Bounded lifecycle cleanupShutdown must actually release threads, connections, and files.

The Ed25519 signature binds:

  • the component catalog,
  • the runtime manifest,
  • the optional configuration metadata,
  • the bundle identity and version, and
  • the publisher key id.

It authenticates those exact bytes. Understanding what it does not do matters just as much:

Verification is offline and code-free:

Terminal window
./yano.sh appchain plugin inspect <jar> --trust-key <key-id>=<64-hex-public-key>
./yano.sh appchain plugin validate <jar> --trust-key <key-id>=<64-hex-public-key> \
--output catalog-snapshot.json
./yano.sh appchain metadata verify <jar> --trust-key <key-id>=<key-hex>

Neither command loads provider classes, runs plugin code, fetches a registry, nor installs the JAR. The public key is not secret; distribute it freely.

When a project selects a custom capability, appchain.lock pins:

  • the signed, data-only catalog snapshot under component-catalogs/;
  • the catalog, runtime manifest, and configuration-metadata digests; and
  • the complete plugin-JAR digests.

render and doctor reverify the snapshot automatically, and a JAR that does not match the pinned digest fails artifact readiness. That is how a “works on my node” plugin mismatch becomes a build error rather than a stalled chain.

Typed configuration metadata is what makes appchain config validate and appchain explain useful for third-party plugins. Each property declares its type, default, bounds, allowed values, scope, change policy, and secret flag.

Coverage is reported honestly. Treat custom-plugin metadata as PARTIAL unless Yano reports FULL coverage, and verify the signed metadata and its runtime-manifest binding before trusting a third-party artifact.

The scope field is the one to read carefully:

ScopeMeaning
CONSENSUS_SHAREDMust be identical on every member. A mismatch diverges the state root.
Node-localPorts, storage, credentials, executor placement. Safe to differ.

And the change policy:

PolicyMeaning
NEW_CHAIN_REQUIREDThe value is part of chain identity. Changing it means a new chain, or a governed activation.
OthersSee the generated configuration reference.
You want to…Implement
Interpret new message bodies and own new stateAppStateMachine + AppStateMachineProvider
Arrange existing components in a new committed orderA composite profile provider
Perform an authorized external actionAn effect executor
Deliver finalized blocks somewhereA finalized-stream sink
Expose a bounded read surface over your stateA domain API and committed queries
Prove an application-level factProofSubjectProvider
Hold keys outside the nodeA signer
React to Cardano deposits or metadata labelsAn L1 observer

For a proof subject, put its descriptorDigest in the capability manifest; the runtime activates the provider only when subject id, version, component id, and digest all match. Resolution must be deterministic and side-effect free — see State and proofs.