Metadata-Version: 2.5
Name: onelastleaf-plugin-sdk
Version: 0.1.0
Summary: Python SDK for onelastleaf process plugins
Project-URL: Repository, https://github.com/onelastleaf/python-plugin-sdk
Project-URL: Issues, https://github.com/onelastleaf/python-plugin-sdk/issues
Author: onelastleaf contributors
Maintainer: onelastleaf contributors
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: asyncio,grpc,onelastleaf,plugin,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: grpcio<2,>=1.75.1
Requires-Dist: protobuf<7,>=6.31.1
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: grpcio-tools==1.75.1; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.12; extra == 'dev'
Requires-Dist: types-grpcio>=1.75; extra == 'dev'
Requires-Dist: types-protobuf>=6.30; extra == 'dev'
Description-Content-Type: text/markdown

# onelastleaf Python plugin SDK

This repository contains the asyncio/gRPC runtime used by Python plugins for
[onelastleaf](https://github.com/onelastleaf/onelastleaf). The package name is
`onelastleaf-plugin-sdk`; Python code imports it as
`onelastleaf_plugin_sdk`.

If you are here to try the SDK, there are two useful paths:

- work on and test the SDK itself;
- generate a small Python plugin and let oll install and run it.

Both are covered below. Python 3.11 or newer is required. The SDK is tested on
Linux, macOS, and Windows with Python 3.11 through 3.14.

## Install the SDK

Install the published release inside your project's virtual environment:

```sh
python -m pip install onelastleaf-plugin-sdk==0.1.0
```

Most plugin authors do not need to install the SDK by hand: the project
generated by `oll plugin new` already declares this exact dependency.

## Work on the SDK

### Create a development environment

Keep the SDK's dependencies in a virtual environment inside this checkout:

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
```

`-e .` installs the checkout in editable mode, so source changes are visible
without reinstalling it. The `dev` extra adds the test, lint, type-check,
package-build, and pinned protobuf-generation tools; plugin users do not install
those tools.

If `python3 -m venv` cannot create an environment, install your operating
system's Python venv/ensurepip package first. You should not need `sudo pip` or
an installation into the system Python.

### Run the tests

With the environment still active:

```sh
python -m pytest
ruff format --check src tests tools
ruff check src tests tools
mypy
python tools/generate_protocol.py --check
```

The tests cover handshake validation, full trace propagation, concurrent job
completion/cancellation races, late host responses, stdin liveness, and
bounded-memory artifact streaming. The other commands check formatting and
typing and prove that the checked-in protocol bindings are reproducible.

### Build a wheel and source archive

Python does not have a separate compile step for this SDK. The closest release
build is:

```sh
python -m build
```

This writes a wheel and source archive to `dist/`. Building is not required
while developing with an editable install.

### Sync and regenerate the protocol

The main onelastleaf repository is the protocol authority. Pull the current
files from GitHub rather than relying on another checkout that happens to be on
your machine:

```sh
protocol_source="$(mktemp -d)"
git clone --depth 1 https://github.com/onelastleaf/onelastleaf.git \
  "$protocol_source/onelastleaf"
for name in common config document plugin; do
  cp "$protocol_source/onelastleaf/proto/oll/$name.proto" proto/oll/
done
python tools/generate_protocol.py
python tools/generate_protocol.py --check
```

`grpcio-tools==1.75.1` is pinned in the development extra. Its generated gRPC
guard therefore agrees with the runtime dependency floor `grpcio>=1.75.1`, and
its protobuf generator agrees with `protobuf>=6.31.1`. Only
`src/onelastleaf_plugin_sdk/protocol/` is generated; there is no second output
tree to keep in sync. CI runs Python 3.11 through 3.14 against those exact
runtime floors, so the declared minimums are exercised rather than merely
accepted by the resolver.

## Try the SDK with oll

Install the matching released `oll` binary and put it on `PATH`, then follow the
[main project's quick start](https://github.com/onelastleaf/onelastleaf#quick-start)
to initialize and start the local daemon. You will also need a remote Git
repository that the daemon can clone.

A plugin cannot be run by pointing it at a public oll port: oll creates a
private loopback gRPC endpoint for each plugin process.

### 1. Generate a Python plugin

Choose a new destination and stable plugin ID:

```sh
oll plugin new hello-python \
  --language python \
  --id dev.example.hello \
  --name hello-python
cd hello-python
```

Generation only writes files. It does not create a venv, install packages,
initialize Git, or contact the daemon. The generated project contains an
`echo` action, a test, `pyproject.toml`, and the publisher manifest `oll.toml`.

### 2. Install the published SDK dependency

The generated `pyproject.toml` pins the published SDK release:

```toml
dependencies = ["onelastleaf-plugin-sdk==0.1.0"]
```

Leave this dependency as published. During local plugin development, `pip`
downloads it from the configured Python package index. During plugin
installation, oll creates a fresh managed venv and the generated source recipe
downloads the same pinned release there.

### 3. Test the plugin as a normal Python project

The plugin gets its own development environment. It is separate from the SDK
checkout's `.venv`:

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e . pytest
python -m pytest
deactivate
```

Do not start the generated console script directly as an end-to-end test. The
script expects `OLL_PLUGIN_ENDPOINT` and a parent-liveness stdin pipe, both of
which are supplied by oll when it supervises the process.

### 4. Commit, install, and call the plugin

oll installs source plugins from Git, so commit the project and push it to a
remote the daemon can access:

```sh
git init -b main
git add .
git commit -m "feat: add hello Python plugin"
git remote add origin https://github.com/you/hello-python.git
git push -u origin main
```

Then install and start it:

```sh
oll plugin install https://github.com/you/hello-python.git --source
oll plugin start dev.example.hello
oll plugin info dev.example.hello
```

Call the generated action:

```sh
oll plugin call dev.example.hello echo -- hello from python
oll job info <job-id-printed-by-the-previous-command>
```

`plugin call` returns after the job is admitted and prints its job ID. Use
`job info` to inspect the result. Runtime output is available through:

```sh
oll plugin log dev.example.hello
```

After changing the plugin, commit and push again, then publish the new
generation and restart the running process:

```sh
oll plugin update dev.example.hello
oll plugin restart dev.example.hello
```

A successful update deliberately does not restart a running plugin for you.

## Why there are several virtual environments

Python venvs can feel confusing here because they solve three different
problems:

| Environment | Owner | Purpose |
| --- | --- | --- |
| SDK checkout `.venv` | you | Run this repository's tests and build packages. |
| Plugin checkout `.venv` | you | Run the plugin's own tests while developing it. |
| Installed generation `.venv` | oll | Run the exact plugin generation managed by the daemon. |

The first two are disposable local development environments. oll neither reads
nor reuses them. The generated Python `oll.toml` instead declares this recipe:

```toml
[source]
checkout = "generation"
steps = [
  ["python3", "-m", "venv", "{generation}/.venv"],
  ["{generation}/.venv/bin/pip", "install", "{generation}"],
]

[runtime]
argv = ["{generation}/.venv/bin/hello-python"]
```

oll places the complete Git checkout in its final UUID-named generation before
creating the venv. That ordering is important because venv scripts contain
absolute paths and may break if their directory is renamed afterward. Only
after the build is validated does oll atomically switch `current` to the new
generation; a failed update leaves the old one current. On startup, oll also
cleans up unreferenced generations left behind by a crash.

The installed generation keeps the source checkout, its `.git` directory, the
venv, and other build output. Do not commit secrets, and expect an installed
Python plugin to use more disk space than its source tree alone.

This checkout mode is selected by the publisher manifest, not guessed from the
language, and a user mask cannot change it. Generation-mode recipes may refer
to `{generation}` and `{mask_dir}`; `{source}` and `{install}` are not available
in this mode. Release installation ignores the source checkout recipe and still
requires a relocatable published artifact.

## Runtime model in plain English

The plugin does not host a server. oll hosts a loopback TCP gRPC server on an
ephemeral port, starts the plugin as a trusted child process, and passes the
address in `OLL_PLUGIN_ENDPOINT`. The SDK connects to that address, completes
the protocol handshake, and dispatches actions registered with
`Plugin.action`.

The child's stdin is reserved for parent liveness. EOF means the oll parent is
gone, so the SDK exits; stdin is not available for plugin commands or user
input. stdout and stderr are captured in per-plugin logs.

A minimal action looks like this:

```python
import asyncio

from onelastleaf_plugin_sdk import ActionResult, Plugin

plugin = Plugin("dev.example.hello", "0.1.0")


@plugin.action("echo", "Return the supplied arguments")
async def echo(_context, arguments: list[str]) -> ActionResult:
    return ActionResult.string(" ".join(arguments))


asyncio.run(plugin.run())
```

The generated project wraps this in a console entry point so oll can start it
from the managed venv.

The action context owns a host API already bound to that job and its trace, so
normal plugin code does not pass `job_id` or trace messages around:

```python
from onelastleaf_plugin_sdk.protocol import common_pb2

configured = await context.host.get_config()
document = await context.host.read_document(request)
await context.host.log(
    common_pb2.LOG_LEVEL_INFO,
    "my-plugin",
    "work completed",
)
```

Artifacts use a seekable binary source. The SDK hashes it outside the event
loop, negotiates the transfer, and sends one bounded chunk at a time:

```python
from io import BytesIO

from onelastleaf_plugin_sdk import ArtifactInput

descriptor = await context.host.store_artifact(
    ArtifactInput(
        artifact_id="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
        file_name="result.txt",
        media_type="text/plain",
        source=BytesIO(b"result"),
    )
)
```

`context.deadline` is informational. oll owns deadline enforcement and sends a
job-scoped cancellation request when it expires. The SDK sets
`context.cancellation`, cancels only that action task, keeps serving heartbeats
and other jobs, and acknowledges cancellation only after the action has
stopped.

## Protocol evolution

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 plugin envelope has no SDK-level encoded-size cap. Both gRPC directions are
configured as unlimited so Python's usual 4 MiB receive default is not retained.
Artifact data still follows the smaller chunk size negotiated in `HostHello`.

## Common problems

- **`No module named onelastleaf_plugin_sdk`**: activate the intended venv and
  run `python -m pip install -e .` in the SDK or plugin checkout as appropriate.
- **`OLL_PLUGIN_ENDPOINT is required`**: the plugin was started by hand.
  Install and start it through oll for an end-to-end run.
- **Installation fails while running pip**: inspect the diagnostic's retained
  build-log path. The daemon user needs `python3` with venv support and access
  to every package source named by `pyproject.toml`.
- **New code is installed but old behavior is still running**: `plugin update`
  does not restart the process; run `oll plugin restart <id-or-name>`.
