Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ohemilyy/universe/llms.txt

Use this file to discover all available pages before exploring further.

Universe’s extension system lets you ship new capabilities as standalone JARs without modifying the orchestrator itself. You can add a custom container runtime, a new remote storage backend, or a set of dynamic template variables — all by implementing one of the provider interfaces and registering it in your extension’s onLoad() hook.

The Extension interface

Every extension must implement Extension from :extensions:extension-api:
interface Extension {
    fun id(): String
    fun version(): String

    fun onLoad()
    fun onUnload()
    fun onReload()
}
id() must be unique across all loaded extensions. version() is used only for display in extension list. The three lifecycle methods are called by Universe at startup, shutdown, and when extension reload is issued. A minimal extension that does nothing but log its presence looks like this:
class ExampleExtension : Extension {
    override fun id(): String = "example-extension"
    override fun version(): String = "0.0.1"

    override fun onLoad() {
        log("ExampleExtension loaded!", LogType.SUCCESS)
    }

    override fun onUnload() {
        log("ExampleExtension unloaded!", LogType.SUCCESS)
    }

    override fun onReload() {
        log("ExampleExtension reloaded!", LogType.SUCCESS)
    }
}

Creating a runtime extension

Implement RuntimeProvider from :api and register it with RuntimeRegistry in onLoad().

1. Implement RuntimeProvider

interface RuntimeProvider {
    fun start(
        instanceId: String,
        workingDir: Path,
        port: Int,
        command: String,
        ramMB: Int,
        cpu: Int
    ): ProcessHandle

    fun stop(instanceId: String)
    fun executeCommand(instanceId: String, command: String)
    fun isRunning(instanceId: String): Boolean
}
MethodResponsibility
startLaunch the process or container. Return a ProcessHandle representing it.
stopTerminate the process or container for the given instance.
executeCommandPipe a command string into the running process’s stdin (or equivalent).
isRunningReturn true if the process or container is still alive.

2. Inject RuntimeRegistry and register in onLoad()

class MyRuntimeExtension : Extension {
    override fun id() = "runtime-my-provider"
    override fun version() = "1.0.0"

    @Inject
    private lateinit var runtimeRegistry: RuntimeRegistry

    override fun onLoad() {
        runtimeRegistry.register("my-provider", MyRuntimeProvider())
    }

    override fun onUnload() {
        runtimeRegistry.unregister("my-provider")
    }

    override fun onReload() {
        onUnload()
        onLoad()
    }
}
Once registered, operators can use the runtime by setting "runtime": "my-provider" in any instance configuration file.

Creating a storage extension

Implement TemplateStorageProvider from :extensions:extension-api and register it with TemplateStorageRegistry.

1. Implement TemplateStorageProvider

interface TemplateStorageProvider {
    val storageKey: String

    fun downloadTemplate(group: String, name: String): Boolean
    fun uploadTemplate(group: String, name: String): Boolean
    fun listTemplates(group: String): List<String>
}
storageKey is the value operators write in the "storage" field of a template reference (e.g., "s3", "ftp"). Return true from downloadTemplate and uploadTemplate to signal success.

2. Register in onLoad()

class MyStorageExtension : Extension {
    override fun id() = "storage-my-backend"
    override fun version() = "1.0.0"

    @Inject
    private lateinit var templateStorageRegistry: TemplateStorageRegistry

    override fun onLoad() {
        templateStorageRegistry.register(MyStorageProvider())
    }

    override fun onUnload() {
        templateStorageRegistry.unregister("my-backend")
    }

    override fun onReload() {
        onUnload()
        onLoad()
    }
}
Template references in instance configurations can then declare "storage": "my-backend".

Adding custom template variables

Implement TemplateVariableProvider and register it with TemplateVariableRegistry. Your variables are merged with the built-in set (%PORT%, %INSTANCE_ID%, %MASTER_IP%, etc.) during instance deployment.
interface TemplateVariableProvider {
    fun provideVariables(): Map<String, String>
}
class MyVariableProvider : TemplateVariableProvider {
    override fun provideVariables(): Map<String, String> = mapOf(
        "%MY_CUSTOM_VAR%" to "some-value"
    )
}
Register it inside your extension’s onLoad():
@Inject
private lateinit var templateVariableRegistry: TemplateVariableRegistry

override fun onLoad() {
    templateVariableRegistry.register(MyVariableProvider())
}
Any file listed in Configuration.fileModifications will have %MY_CUSTOM_VAR% replaced with "some-value" when an instance is created.

Build rules

Your extension’s build.gradle.kts must follow these constraints:
dependencies {
    // Only these two are allowed as compile-time dependencies
    compileOnly(projects.api)
    compileOnly(projects.extensions.extensionApi)

    // External libraries must use runtimeDownload so Universe fetches them at startup
    runtimeDownload("com.example:my-library:1.0.0")
}
Never add :app as a dependency. The app module is not part of the stable extension API and its internals change between versions. Any extension that imports app classes will break silently on upgrade.
Use runtimeDownload for all external JVM libraries (AWS SDK, HTTP clients, etc.). This keeps your JAR small and lets Universe’s classloader manage dependency resolution.

JAR placement and naming

Name your JAR after its category and function, following the convention used by built-in extensions:
CategoryConventionExample
Runtimeruntime-<name>.jarruntime-docker.jar
Storagestorage-<name>.jarstorage-s3.jar
General<name>.jarmy-extension.jar
Drop the JAR into ./extensions/ and restart Universe (or run extension reload if hot-reload is sufficient for your extension’s setup):
cp my-extension.jar ./extensions/
# then restart, or:
extension reload
If your extension reads its own config, place it at ./extensions/<extension-id>/config.json to match the convention used by the built-in extensions.

Build docs developers (and LLMs) love