onelastleaf Elixir plugin SDK
This is the official Elixir runtime for writing onelastleaf process plugins. You define the actions. The SDK looks after the long-lived gRPC session with oll, including job routing, cancellation, host calls, logging, artifacts, and shutdown.
If you are here to try the SDK itself, start with the next section. If you want to build a plugin, jump to Create a plugin.
Build and test this SDK
You need Elixir 1.16 or newer, a compatible Erlang/OTP installation, and Mix. Mix may offer to install Hex and Rebar the first time it fetches or builds dependencies.
The generated protobuf modules are already checked in, so the protobuf code-generation toolchain is not needed for a normal build. A change under proto/ is different: it must be regenerated as part of a coordinated protocol release.
From this repository:
mix deps.get
mix compile
mix test
mix format --check-formatted
mix docs --warnings-as-errors
mix compile builds the library in _build/; there is no standalone SDK executable. To make the same Hex archive that would be published, run:
mix hex.build
The unit tests cover the public runtime helpers. The formatting command checks the library, unit tests, and internal test fixtures.
Message-size behavior
Can grpc 1.0.4 enforce a finite receive limit? No. Its Elixir client does
not expose that setting, and it buffers a complete frame before the SDK sees the
decoded message. Checking the size afterwards would be too late to protect
memory, so this SDK does not label such a check as a transport limit.
That matches the current oll contract: PluginEnvelope has no finite protocol
cap, and the SDK adds no smaller application cap. The effective ceiling is
therefore whatever the library, platform, and available memory can handle.
Artifact chunks are different and remain bounded by the size from HostHello.
If oll ever requires a finite receive cap, this SDK will need upstream support
in elixir-grpc/grpc or a maintained transport patch.
Create a plugin
For the full workflow, you also need a compatible oll CLI and a running daemon. The SDK supports Elixir 1.16 and newer. oll plugin new currently puts ~> 1.17 in a generated project's mix.exs, but that is a template default, not an SDK requirement; plugin authors can change it to another SDK-supported version.
The daemon needs system Git and must be able to reach the plugin's remote and fetch its Mix dependencies. Source installation uses the POSIX install command too; oll plugin new records both mix and install as build requirements in the generated manifest.
The easiest route is to let oll create the project skeleton:
oll plugin new my-echo-plugin \
--language elixir \
--id example.echo \
--name my-echo-plugin
cd my-echo-plugin
mix deps.get
mix test
mix escript.build
The generated project contains an escript entry point, an echo action, tests, and the oll.toml recipe used to install and launch it. The plugin ID is its permanent identity, so choose it once and keep the same value in both the Elixir code and oll.toml.
The current template pins SDK version 0.1.0:
{:onelastleaf_plugin_sdk, "== 0.1.0"}
If you are changing this SDK locally, point a nearby plugin project at this checkout while you run Mix yourself:
{:onelastleaf_plugin_sdk, path: "../elixir-plugin-sdk"}
Do not publish that relative path and expect oll plugin install to find it: oll builds from a private Git checkout that cannot see sibling directories on your development machine. Before installation, use the released Hex package or another dependency source reachable from that checkout.
The generated mix.exs tells Mix which module starts the escript:
def project do
[
# ...
escript: [main_module: MyEchoPlugin],
deps: [{:onelastleaf_plugin_sdk, "== 0.1.0"}]
]
end
That module can be as small as this:
defmodule MyEchoPlugin do
alias Onelastleaf.PluginSDK.{ActionResult, Plugin}
def main(_process_arguments) do
Plugin.new!("example.echo", "0.1.0")
|> Plugin.action("echo", "Return the supplied arguments", fn _context, arguments ->
ActionResult.string(Enum.join(arguments, " "))
end)
|> Plugin.run!()
end
end
Plugin.run!/1 blocks for the lifetime of the plugin process. Action handlers receive an ActionContext and the ordered string arguments passed to oll plugin call. Return an ActionResult; helpers are available for string, boolean, and integer results.
For longer-running work, call ActionContext.cancelled?/1 or ActionContext.check_cancellation!/1 regularly. The context is also how an action reads configuration, invokes configuration functions, calls other host capabilities, writes structured logs, and stores artifacts.
Connect it to oll
Plugins do not open a server and should not invent their own endpoint. When oll starts the installed escript, it:
- opens the loopback gRPC server and provides its address in
OLL_PLUGIN_ENDPOINT; - uses the plugin's stdin as a parent-liveness pipe, so EOF means the plugin must exit; and
- captures stdout and stderr in the per-plugin log.
The SDK implements that contract inside Plugin.run!/1. In normal use, do not set OLL_PLUGIN_ENDPOINT yourself and do not use stdin for application input.
oll plugin new writes the required manifest for you. For reference, the generated source-mode recipe has this shape:
format_version = 1
[plugin]
id = "example.echo"
name = "my-echo-plugin"
[source]
checkout = "source"
steps = [
["mix", "deps.get"],
["mix", "escript.build"],
[
"install", "-m", "755", "{source}/my-echo-plugin",
"{install}/my-echo-plugin",
],
]
[source.dependencies]
"install" = "Install a POSIX install utility and ensure it is in PATH."
"mix" = "Install Elixir with Mix and ensure mix is in PATH."
[runtime]
argv = ["{install}/my-echo-plugin"]
This SDK follows the canonical protobuf wire contract. It never computes, embeds, publishes, or compares a schema hash or fingerprint. Descriptor-wide hashes change for compatible additions and unrelated services, so they reject valid peers. Protocol changes instead preserve field numbers and wire types, give additions safe absent semantics, and tolerate unknown fields. Exact SDK pins provide reproducible builds; they are not protobuf API versioning.
The recipe commands are argument arrays, not shell snippets, and oll runs
them from a private Git checkout before publishing the finished install.
Commit the generated plugin to a Git repository that the oll daemon can reach, then install and exercise it:
oll plugin install https://github.com/your-name/my-echo-plugin.git --source
oll plugin start example.echo
oll plugin call example.echo echo -- hello from Elixir
oll job info <job-id-printed-by-plugin-call>
oll plugin log example.echo
Installation builds the plugin but leaves it stopped; plugin start records that it should be running. plugin call returns after oll has admitted the job and the plugin has accepted it, so use the printed job ID with oll job info to inspect its eventual result.
When you are finished:
oll plugin stop example.echo
Use oll plugin update example.echo after pushing a new commit to the configured branch. A successful update publishes the new build but does not restart a running plugin automatically; restart it explicitly when you are ready.
A useful testing split
Keep action logic in ordinary functions and unit-test those with ExUnit. Test the process boundary separately through oll: the endpoint, handshake, job routing, and stdin behavior only exist when oll owns the plugin process. This keeps most tests fast without pretending that a directly launched escript is a real plugin session.
License
onelastleaf_plugin_sdk is licensed under the GNU General Public License,
version 3 or (at your option) any later version. See LICENSE.