Packages

Official onelastleaf process-plugin SDK for Elixir.

Current section

Files

Jump to

README.md

# onelastleaf Elixir plugin SDK
This is the official Elixir runtime for writing [onelastleaf](https://github.com/onelastleaf/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](#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:
```console
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:
```console
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:
```console
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:
```elixir
{: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:
```elixir
{: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:
```elixir
def project do
[
# ...
escript: [main_module: MyEchoPlugin],
deps: [{:onelastleaf_plugin_sdk, "== 0.1.0"}]
]
end
```
That module can be as small as this:
```elixir
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:
```toml
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:
```console
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:
```console
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](LICENSE).