223 lines
7.9 KiB
Markdown
223 lines
7.9 KiB
Markdown
# Harness Plugin Interface
|
|
|
|
Omnigent now discovers optional harness support through Python entry points.
|
|
Core `omnigent` ships the built-in harness contribution. A separate package, for
|
|
example `omnigent-kimi`, can add harness ids, aliases, runner modules, install
|
|
metadata, model environment plumbing, and picker labels without adding that
|
|
harness to the default install.
|
|
|
|
The goal is:
|
|
|
|
- `pip install omnigent` gives only core harnesses.
|
|
- `pip install omnigent-kimi` adds Kimi support to the same `omni` CLI and
|
|
server process.
|
|
- Core can still produce a targeted error for known optional harness ids:
|
|
install `omnigent-kimi`.
|
|
|
|
## Package Contract
|
|
|
|
An optional harness package declares an entry point in the
|
|
`omnigent.community.harness` group. Community harness implementation modules
|
|
must also live under the `omnigent.community.harness.*` namespace; core rejects
|
|
plugins that try to register flat packages or override builtin harness names.
|
|
|
|
```toml
|
|
[project]
|
|
name = "omnigent-foo"
|
|
dependencies = [
|
|
"omnigent==0.3.0.dev0",
|
|
]
|
|
|
|
[project.entry-points."omnigent.community.harness"]
|
|
foo = "omnigent.community.harness.foo.plugin:get_contribution"
|
|
```
|
|
|
|
For local sibling checkouts, keep the package dependency normal and point uv at
|
|
the local core checkout:
|
|
|
|
```toml
|
|
[tool.uv.sources]
|
|
omnigent = { path = "../omnigent-oss-2", editable = true }
|
|
```
|
|
|
|
If the plugin lives inside the core repo, the relative path should point back to
|
|
the repo root. If it moves to a sibling repo, update the path. A bad path is why
|
|
uv may try to build `omnigent @ file:///Users/<user>`.
|
|
|
|
## Registry Types
|
|
|
|
The public interface lives in `omnigent.harness_plugins`:
|
|
|
|
```python
|
|
from omnigent.harness_plugins import HarnessContribution
|
|
from omnigent.harness_install_spec import HarnessInstallSpec
|
|
```
|
|
|
|
`HarnessInstallSpec` intentionally lives outside `omnigent.onboarding` so a
|
|
plugin can be imported during entry-point discovery without pulling in the
|
|
provider/onboarding stack and creating import cycles.
|
|
|
|
### `HarnessContribution`
|
|
|
|
Each plugin exports a `get_contribution()` function returning
|
|
`HarnessContribution`.
|
|
|
|
```python
|
|
def get_contribution() -> HarnessContribution:
|
|
return HarnessContribution(
|
|
name="omnigent-foo",
|
|
valid_harnesses=frozenset({"foo"}),
|
|
harness_modules={
|
|
"foo": "omnigent.community.harness.foo.inner.foo_harness",
|
|
},
|
|
aliases={
|
|
"foo-code": "foo",
|
|
},
|
|
install_specs={
|
|
"foo": HarnessInstallSpec(
|
|
"Foo",
|
|
"foo",
|
|
package=None,
|
|
install_hint="curl -fsSL https://foo.example/install.sh | bash",
|
|
login_args=("login",),
|
|
logout_args=("logout",),
|
|
),
|
|
},
|
|
harness_install_keys={
|
|
"foo": "foo",
|
|
"foo-code": "foo",
|
|
},
|
|
missing_install_package={
|
|
"foo": "omnigent-foo",
|
|
"foo-code": "omnigent-foo",
|
|
},
|
|
harness_labels={"foo": "Foo"},
|
|
)
|
|
```
|
|
|
|
## Field Semantics
|
|
|
|
`valid_harnesses`
|
|
: Canonical harness ids accepted by spec validation once the plugin is
|
|
installed.
|
|
|
|
`harness_modules`
|
|
: Maps each canonical harness id to the subprocess module that creates the
|
|
harness app. `omnigent.runtime.harnesses` merges these into `_HARNESS_MODULES`.
|
|
|
|
`aliases`
|
|
: User-facing spellings canonicalized by `omnigent.harness_aliases`, for example
|
|
`foo-code -> foo`.
|
|
|
|
`install_specs`
|
|
: Plugin-provided CLI install/auth metadata, keyed by install key. Use
|
|
`HarnessInstallSpec` from `omnigent.harness_install_spec`.
|
|
|
|
`harness_install_keys`
|
|
: Maps harness ids and aliases to an `install_specs` key. Readiness and
|
|
preflight checks use this to decide which CLI binary a harness requires.
|
|
|
|
`model_env_keys`
|
|
: Maps harness id to an env var name used by launcher/spec generation for model
|
|
override plumbing.
|
|
|
|
`spawn_env_builders`
|
|
: Maps headless harness id to a callable import path. The runner calls this to
|
|
build per-spawn environment variables from the agent spec.
|
|
|
|
`missing_install_package`
|
|
: Maps known optional harness spellings to the package that provides them. Core
|
|
uses this even when the plugin is not installed so validation and process-manager
|
|
errors can say `pip install omnigent-foo`.
|
|
|
|
`harness_labels`
|
|
: Maps canonical harness ids to display labels returned by `GET /v1/harnesses`
|
|
and merged into web picker surfaces.
|
|
|
|
## Runtime Flow
|
|
|
|
1. Python loads installed entry points in `omnigent.community.harness`.
|
|
2. `omnigent.harness_plugins.plugin_state()` merges the built-in contribution
|
|
with each plugin contribution.
|
|
3. Spec validation checks `accepted_harnesses()` and uses
|
|
`missing_install_package()` for known optional harness hints.
|
|
4. `omnigent.runtime.harnesses` registers `harness_modules()`.
|
|
5. Runner launch paths consult `spawn_env_builders()` for contributed headless
|
|
harnesses.
|
|
6. Host readiness uses `harness_install_keys()` and `install_specs()` to gate
|
|
CLI-backed contributed harnesses on their binary.
|
|
7. The server exposes `GET /v1/harnesses` from `harness_catalog()`.
|
|
8. The web UI merges `/v1/harnesses` into harness picker surfaces.
|
|
|
|
## Minimal Headless Harness Checklist
|
|
|
|
For a non-native harness:
|
|
|
|
- Create a separate package, for example `omnigent-foo`.
|
|
- Add the `omnigent.community.harness` entry point.
|
|
- Implement `get_contribution()`.
|
|
- Fill `valid_harnesses`, `harness_modules`, and `aliases`.
|
|
- Add `install_specs` and `harness_install_keys` if the harness needs a CLI.
|
|
- Add `spawn_env_builders` if the harness needs spec-derived env vars.
|
|
- Add `missing_install_package` entries in core if the harness id should produce
|
|
a targeted install hint before the plugin is installed.
|
|
- Move harness implementation modules into the plugin package.
|
|
- Remove the harness id and module from the built-in contribution.
|
|
|
|
## Native TUI Harnesses
|
|
|
|
Community native terminal harnesses are not supported by this interface yet.
|
|
Core native harnesses still use internal registry metadata, but the runner,
|
|
chat-resume, CLI-command, interrupt/stop, and built-in agent seeding paths are
|
|
not pluggable. Community plugins that set `native_harnesses` or `native_agents`
|
|
are rejected at load time until those lifecycle hooks are wired end to end.
|
|
|
|
## Import Rules
|
|
|
|
Entry-point loading happens early and can happen while other core modules are
|
|
still initializing. Plugin `plugin.py` should keep top-level imports light:
|
|
|
|
- safe: `omnigent.harness_plugins`, `omnigent.harness_install_spec`, constants,
|
|
stdlib;
|
|
- risky: `omnigent.onboarding.*`, `omnigent.cli`, server modules, runner modules,
|
|
or anything that imports `omnigent.harness_aliases`.
|
|
|
|
Put heavy imports inside the callable that needs them. For example, a spawn-env
|
|
builder may import provider/runtime helpers inside `build_spawn_env()`, but
|
|
`get_contribution()` should not need onboarding.
|
|
|
|
## Local Demo Commands
|
|
|
|
Sibling checkout demo:
|
|
|
|
```bash
|
|
cd /path/to/omnigent-oss-2
|
|
uv pip install -e .
|
|
uv pip install -e ../omnigent-foo
|
|
|
|
uv run python -c "from omnigent.harness_plugins import valid_harnesses; print('foo' in valid_harnesses())"
|
|
uv run python -c "from omnigent.runtime.harnesses import _HARNESS_MODULES; print(_HARNESS_MODULES['foo'])"
|
|
```
|
|
|
|
If the plugin dependency still points at a published or wrong local `omnigent`,
|
|
use the sibling source override in the plugin `pyproject.toml`:
|
|
|
|
```toml
|
|
[tool.uv.sources]
|
|
omnigent = { path = "../omnigent-oss-2", editable = true }
|
|
```
|
|
|
|
For published packages, remove local source overrides and publish both
|
|
distributions with compatible versions.
|
|
|
|
## Tests To Add For Each Split Harness
|
|
|
|
- Core registry excludes the optional harness by default.
|
|
- Core validation/error messages suggest the optional package.
|
|
- Installing or faking the entry point adds `valid_harnesses`, aliases, install
|
|
specs, and harness modules.
|
|
- Readiness/setup tests isolate core-only behavior by stubbing entry-point
|
|
discovery when the optional package is installed in the dev environment.
|
|
- Two community plugins cannot claim the same harness spelling, alias, or
|
|
install key.
|