Plugins and extensions

Choose the narrowest extension point: Harness middleware, Pydantic AI Capabilities, Environment provider bindings, Environment Run Extensions, or Provider plugins.

Harness exposes focused extension points rather than one universal plugin interface. Choose the narrowest boundary that owns the behavior and lifetime you need.

Extension pointUse it forLifecycleModel-visible
Harness middleware pluginTransform semantic input, observe events, wrap errors, or replace a complete result candidateAgent-bound at build, then freshly Run-bound around each logical RunOnly through an explicit Capability contribution
Pydantic AI CapabilityOwn or compose Toolsets, instructions, request hooks, Agent-loop state, and collaboration with other Run CapabilitiesNative Pydantic Agent/Run lifecycleYes
EnvironmentProviderBindingAcquire a Host-specific session or resource in an async bind() scope and expose provider-neutral Environment operations for one mountOne binding scope inside one EnvironmentRuntimeOnly through explicit Environment tools/context
EnvironmentRunExtensionHold a resource that needs the complete entered Environment aggregate; use EnvironmentRunCallbacks for simple paired callbacksEntered with the current aggregate; reverse-order exit before provider teardownNo
Provider pluginAdd an Environment Provider your Host can selectInert definitions loaded once at Host startupNo

Installed entry-point metadata means code is available, not enabled or authorized. Importing a13n_harness scans no entry points and activates no extension.

Choose and Activate an Extension Point

Availability, selection, and activation are separate decisions:

Extension pointMakes an implementation availableSelects and activates itAutomatic behavior
Harness middleware pluginInstall a package with an a13n_harness.plugins entry point, or import a concrete pluginEnable one configured plugin_key/plugin_id, or pass the concrete plugin to HarnessBuilder.build()Ambient configuration is disabled by default; after explicit opt-in, only entries with enabled: true are loaded
Pydantic AI CapabilityImport a concrete Capability, or let a trusted Host authorize an exact declarative typePut the instance in definition/Run composition, or put its serialized spec in AgentSpec.capabilitiesOptional Capabilities are never inferred from package presence; see Capabilities
EnvironmentRunExtensionInstall a package with an a13n_harness.environment_run_extensions entry point, or import a concrete extensionSelect an exact factory key, create one identified instance, and pass it to create_environment_runtime(extensions=...)There is no ambient configuration or automatic selection
EnvironmentProviderBindingConstruct a fresh trusted binding from the owning Provider layerPut the binding in an EnvironmentRuntimeMountProvider availability never mounts or exposes model tools by itself
Provider pluginInstall a distribution with an a13n_harness.providers.plugins entry pointName that entry point in the Host's enabled-plugin selection, then select a Provider type by nameInstallation activates nothing; an unselected entry point is never imported

AgentSpec selects only Capabilities. It does not select Harness middleware, Environment Run Extensions, Providers, credentials, or live collaborators. A selected plugin may contribute ordinary Capabilities from trusted plugin code, but that contribution is owned by the plugin rather than reconstructed from AgentSpec.

Harness Middleware

Harness plugins are trusted Python middleware around one complete process-local Run. Use a Pydantic AI Capability for behavior inside the Agent loop. Use middleware when behavior must wrap semantic input, the canonical event stream, errors, or the complete result boundary.

A plugin can be supplied directly as an AbstractHarnessPlugin or created from a selected HarnessPluginFactory entry point.

Direct Composition

Direct objects are the simplest choice for an embedded application:

Python
from a13n_harness import HarnessBuilder

plugin = AuditPlugin("audit-primary")
executable = HarnessBuilder().build(
    agent_spec,
    output_type=str,
    model=model,
    plugins=(plugin,),
)

Direct and configured plugins enter the same ordering, Agent binding, Run binding, middleware, result validation, and cleanup path.

Feature Capabilities and Current Provider Clients

A middleware plugin can run alongside a first-party feature Capability without contributing or configuring that Capability itself. The Host selects one configured WebCapability in the Agent definition and supplies current clients and policy separately. Reserved first-party Capability source rules still apply: WebCapability belongs to the definition, not a plugin's get_capabilities() contribution.

Python
from a13n_harness import RunBindings
from a13n_harness.capabilities import WebBinding

result = await executable.run(
    "Read the page",
    bindings=RunBindings.embedded(
        web=WebBinding(client=web_client, policy=web_policy),
    ),
)

Do not add a companion provider Capability to the plugin or put provider objects in plugin YAML. RunBindings.capabilities remains the source for invocation-policy and MCP Capabilities, not passive feature dependencies. A binding cannot enable a missing feature owner, and provider clients never enter HarnessState. The Host owns their lifetime; a plugin's for_run() remains its ordinary middleware-isolation hook, not another feature API.

The offline middleware and Web example exercises a recorder plugin alongside a definition-owned Web feature, fresh typed bindings, and the standard fetch tool without a network request.

Publish a Plugin Factory

Register one no-argument factory class:

TOML
[project.entry-points."a13n_harness.plugins"]
"acme.audit" = "acme_harness.plugin:AuditPluginFactory"

The entry-point name and plugin_key() must match:

Python
from collections.abc import Mapping

from a13n_harness import AbstractHarnessPlugin
from a13n_harness.plugin_factories import (
    HarnessPluginFactory,
    HarnessPluginFactoryContext,
)
from pydantic import BaseModel, JsonValue


class AuditConfiguration(BaseModel):
    mode: str = "metadata"


class AuditPlugin(AbstractHarnessPlugin):
    def __init__(self, plugin_id: str) -> None:
        self._plugin_id = plugin_id

    @property
    def plugin_id(self) -> str:
        return self._plugin_id


class AuditPluginFactory(HarnessPluginFactory):
    @classmethod
    def plugin_key(cls) -> str:
        return "acme.audit"

    def validate_configuration(
        self,
        configuration: Mapping[str, JsonValue],
    ) -> BaseModel:
        return AuditConfiguration.model_validate(dict(configuration))

    def create_plugin(
        self,
        context: HarnessPluginFactoryContext,
    ) -> AbstractHarnessPlugin:
        # context.configuration holds the validated, normalized configuration.
        return AuditPlugin(context.plugin_id)

Implement validate_configuration() to validate and normalize an entry's configuration with a package-owned schema; Harness calls it before create_plugin(). Factory construction is synchronous and side-effect free. create_plugin() returns a fresh concrete plugin for each configured instance. Mutable Run data belongs in the exact Run-bound plugin returned by for_run().

Select Configured Plugins

The Harness plugin document is a data schema, not a required file format:

Python
from a13n_harness import HarnessBuilder
from a13n_harness.plugin_configuration import HarnessBuildContext

configuration = {
    "schema_version": "1",
    "plugins": [
        {
            "plugin_id": "audit-primary",
            "plugin_key": "acme.audit",
            "enabled": True,
            "configuration": {"mode": "metadata"},
        }
    ],
}

context = HarnessBuildContext.from_configuration(configuration)
builder = HarnessBuilder(build_context=context)

The same schema can come from YAML or JSON:

YAML
schema_version: "1"
plugins:
  - plugin_id: audit-primary
    plugin_key: acme.audit
    enabled: true
    configuration:
      mode: metadata
Python
context = HarnessBuildContext.from_file("harness-plugins.yaml")
builder = HarnessBuilder(build_context=context)

Configuration selects stable entry-point keys and never accepts a module:object target. HarnessBuildContext.extensions is a bounded namespaced JSON value forwarded to selected plugin factories; despite its name, it is not a plugin or Environment-extension list and does not enable anything.

Optional Environment-Variable Source

Configured plugins are disabled by default. A deployment can opt into a document selected by environment variables:

Terminal
export A13N_HARNESS_PLUGIN_CONFIG_ENABLED=true
export A13N_HARNESS_PLUGIN_CONFIG_FILE=/etc/a13n/harness-plugins.yaml
Python
builder = HarnessBuilder()

When disabled, builder construction does not read the file or scan package metadata. When enabled, it reads one source: A13N_HARNESS_PLUGIN_CONFIG_JSON if set, otherwise the file named by A13N_HARNESS_PLUGIN_CONFIG_FILE, otherwise harness-plugins.yaml in the current working directory. A missing or invalid source fails construction. It imports only factory keys selected by enabled entries.

Runtime Plugin Directories

A long-lived Host can publish a complete installed distribution in a new immutable directory and add that directory to sys.path before constructing a replacement builder:

Python
import importlib
import sys
from pathlib import Path


def activate_plugin_directory(path: str | Path) -> Path:
    plugin_directory = Path(path).resolve()
    normalized = str(plugin_directory)
    if normalized not in sys.path:
        sys.path.append(normalized)
    importlib.invalidate_caches()
    return plugin_directory

The directory must contain both the import package and standard distribution metadata with the entry point. A loose .py file is not sufficient.

Follow these steps:

  1. Publish into a fresh directory that is not yet searchable.
  2. Validate the complete installation.
  3. Atomically place the directory.
  4. Activate that exact path.
  5. Build a replacement executable.

Do not mutate an already imported release in place or append two releases that own the same plugin key.

A replacement builder with configured plugins explicitly enabled resolves current distribution metadata only for factory keys selected by its enabled entries. A disabled builder still performs no metadata scan. An existing builder retains its selected factory catalog; an existing executable retains its already constructed plugin graph. Keep old executables alive until their active Runs finish.

The Host remains responsible for artifact trust, dependency compatibility, installation locks, directory ordering, and rollback. Harness includes no package installer or process-global mutable plugin registry.

Plugin Lifecycle

A plugin has three distinct phases:

  1. factory selection during builder construction, then plugin creation during each build() for every root or child definition, for configured plugins;
  2. Agent binding once for each built root or child executable;
  3. Run binding and middleware freshly for every logical Run.

Run middleware must preserve single-consumer streaming and yield exactly one structurally valid result candidate. Use try/finally for plugin-owned cleanup. Do not swallow cancellation or convert cleanup failure into clean completion.

Plugins can contribute native Capabilities at Agent binding. Harness calls get_capabilities() on the instance returned by for_agent(); do not extract contributions before that binding. Plugins and their contributed Capabilities should not implement a second tool dispatcher, message history, usage accumulator, or Environment lifecycle.

To support optional grouped presentation, a plugin can expose source factories or a presentation option for its contribution. A Host-owned composition layer can aggregate selected sources into one ToolProxyCapability(groups=...), while retaining required middleware and leaving unrelated tools direct. Alternatively, a ToolProxyPlan can select an unchanged plugin's contributions by exact plugin ID; this needs no plugin-specific interface, generic lookup, or interception of arbitrary plugins. See ToolProxy plugin-contributed sources for an example and duplicate-installation boundaries.

Provider Plugins

A Provider plugin adds one or more Environment Providers your Host can select, through one entry-point group and one immutable manifest. Model, Web, Connector, and Memory definitions use the same definition contract, but a Host composes them in code and selects them through its own ProviderCatalog.

A definition is a frozen value. It declares its stable type, a display_name, the typed configuration and credential models its inputs use, how credentials are required, and optional setup help. Importing it performs no I/O and creates no client:

Python
from a13n_harness.providers.authentication import Authentication, CredentialMode
from a13n_harness.providers.environment.definition import EnvironmentProviderDefinition

ACME_SANDBOX = EnvironmentProviderDefinition(
    type="acme_sandbox",
    display_name="Acme Sandbox",
    configuration_model=AcmeConnectionConfiguration,
    credential_model=AcmeCredential,
    environment_model=AcmeEnvironmentConfiguration,
    construct=_construct,
    describe_environment=_describe,
    runtime_factory=_runtime,
    authentication=Authentication(mode=CredentialMode.required),
    setup_url="https://acme.example/dashboard",
    setup_label="Acme dashboard",
    supports_stop=True,
    supports_destroy=True,
)

One distribution exports one ProviderManifest per entry point:

Python
from a13n_harness.providers.plugins import ProviderManifest

manifest = ProviderManifest(api_version=1, environment=(ACME_SANDBOX,))
TOML
[project.entry-points."a13n_harness.providers.plugins"]
acme = "acme_providers:manifest"

A Host names the entry points it trusts and builds one Environment catalog from its built-in and selected definitions:

Python
from a13n_harness.providers.catalog import ProviderCatalog
from a13n_harness.providers.plugins import load_provider_plugins

plugins = load_provider_plugins(("acme",))
environments = ProviderCatalog(
    item for plugin in plugins for item in plugin.manifest.environment
)
definition = environments.require("acme_sandbox")

Selection is explicit at every step. Installing the distribution activates nothing; an entry point you do not name is never imported; and a catalog rejects a type that duplicates another definition in the same domain. require() raises ProviderNotSelected for a type this deployment does not offer, so a Host can report a safe configuration error instead of failing unexpectedly.

The runnable plugin example publishes one manifest and a separate Harness middleware plugin from the same project. The installed Provider plugin example shows direct use and Harness UI loading.

Harness Extras

The base a13n-harness installation contains every built-in Provider definition, so metadata, schemas, and Host configuration forms, such as the Console forms in Service, work without an optional dependency. Vendor SDKs are separate extras:

ExtraAddsNeeded by
dockerThe Docker SDK for PythonThe docker Environment Provider
e2bThe asynchronous E2B SDKThe e2b Environment Provider
modalThe Modal SDKThe modal Environment Provider
Terminal
uv add "a13n-harness[docker,e2b]"

Importing Harness or reading a Provider's metadata never imports these SDKs. A Provider whose extra is missing fails with a bounded configuration error when it is actually opened, not at import.

Environment Inputs and Advanced Bindings

When a Provider has already constructed an Environment, pass it directly to run(environment=...) or wrap it in EnvironmentMount to select a permission ceiling and paths. Explicit runtimes and their dynamic mount() and replace() methods accept the same inputs. Harness owns entry and local cleanup; Host code does not need to implement a forwarding binding class:

Python
from a13n_harness.environment import (
    FILE_ACTIONS,
    EnvironmentMount,
    EnvironmentPermissionSet,
)
from a13n_harness.environment.advanced import create_environment_runtime

environment_runtime = create_environment_runtime(
    mounts={
        "workspace": EnvironmentMount(
            environment=environment,
            permission_ceiling=EnvironmentPermissionSet(operations=FILE_ACTIONS),
        ),
    },
    default_mount="workspace",
)

permission_ceiling accepts any exact action set, which is useful for a setup extension that needs only selected file operations. Provider permissions always narrow the ceiling. A runtime takes ownership of each underlying Environment only once, even if it is wrapped in another EnvironmentMount. Invalid initial routes do not take ownership, and a failed attempt to reuse the Environment cannot close its existing scope.

Advanced Provider Binding Scopes

Use EnvironmentProviderBinding with EnvironmentRuntimeMount when a Host must acquire an authenticated session or another resource inside a custom async bind() scope. The binding then exposes provider-neutral file, shell, process, output, port, readiness, and portable-state operations. This advanced input remains accepted by explicit runtime construction and dynamic mount replacement. An existing Environment should use the direct inputs above.

That is a low-level runtime binding contract. Provider catalogs, Environment Provider lifecycle operations, credential handling, and durable provider state belong to Provider plugins and the Host, not to Harness middleware.

An EnvironmentProviderBinding is fresh and single-use. Effectful allocation, authentication, session entry, maintenance tasks, and cleanup-producing work belong inside its async bind() scope or in the owning provider layer, never in import-time discovery or an inert factory constructor.

Environment Run Extensions

Use an EnvironmentRunExtension when setup and teardown need the stable complete EnvironmentRuntime, including an empty, single-mount, or multi-mount runtime.

Callback Composition

For ordinary Host setup and cleanup, register an EnvironmentRunCallbacks adapter instead of defining an extension class:

Python
from a13n_harness.environment import (
    EnvironmentRunCallbacks,
    EnvironmentRunExtensionContext,
)


async def prepare_environment(
    context: EnvironmentRunExtensionContext,
) -> None:
    await context.environment.files.write_text(
        "/workspace/.active-run",
        f"{context.run_id}\n",
        mode="create",
    )


async def clean_environment(
    context: EnvironmentRunExtensionContext,
) -> None:
    await context.environment.files.remove(
        "/workspace/.active-run",
    )


active_run_callbacks = EnvironmentRunCallbacks(
    extension_id="workspace-marker",
    on_enter=prepare_environment,
    on_exit=clean_environment,
)

on_enter runs after portable Environment state restoration and before runtime activation. It participates in making the runtime active; it does not mean every operation family is globally ready. Call context.environment.ensure_ready() when setup depends on an exact family. on_exit runs during reverse-order Environment teardown while provider-neutral operations remain available. It also runs after failure, cancellation, or rollback following successful entry, so it is cleanup rather than a success notification.

Each adapter is one identified extension. Multiple adapters enter in registration order and exit in reverse order. Callback failures use the same authoritative failure semantics as custom extension setup and cleanup. Catch expected failures inside a callback only when the Host deliberately wants best-effort behavior.

Custom Resource Scope

Use a custom async context manager when setup and cleanup share local state or need a richer resource scope:

Python
from contextlib import asynccontextmanager

from a13n_harness.environment import EnvironmentRunExtensionContext


class WorkspaceMarkerExtension:
    def __init__(self, extension_id: str) -> None:
        self._extension_id = extension_id

    @property
    def extension_id(self) -> str:
        return self._extension_id

    @asynccontextmanager
    async def bind(self, *, context: EnvironmentRunExtensionContext):
        path = "/workspace/.active-run"
        await context.environment.files.write_text(
            path,
            f"{context.run_id}\n",
            mode="create",
        )
        try:
            yield
        finally:
            await context.environment.files.remove(path)

Register direct extension objects on an explicit runtime with ordinary Environment inputs:

Python
from a13n_harness.environment.advanced import create_environment_runtime


environment_runtime = create_environment_runtime(
    mounts={"workspace": environment},
    default_mount="workspace",
    extensions=(WorkspaceMarkerExtension("workspace-marker"),),
)

Give the runtime to the Harness through fresh Run bindings. Harness binds, activates, and closes it:

Python
from a13n_harness import RunBindings

bindings = RunBindings.embedded(environment=environment_runtime)
result = await executable.run("Use the prepared workspace", bindings=bindings)

The same extensions= sequence accepts callback adapters and custom extension objects together:

Python
environment_runtime = create_environment_runtime(
    mounts=mounts,
    default_mount="workspace",
    extensions=(
        active_run_callbacks,
        WorkspaceMarkerExtension("custom-marker"),
    ),
)

Extensions enter after providers are available and portable Environment state is restored. They exit in reverse order while the Environment is still open and before provider scopes close.

Explicit Extension Factories

A distribution can register a side-effect-free factory under:

TOML
[project.entry-points."a13n_harness.environment_run_extensions"]
"acme.workspace-marker" = "acme_environment.extension:WorkspaceMarkerFactory"

The Host explicitly selects keys with build_environment_run_extension_factory_catalog() or provides exact factories. Harness owns no ambient configuration document for Environment Run Extensions. A complete installed-factory path is:

Python
from a13n_harness import RunBindings
from a13n_harness.environment import (
    EnvironmentRunExtensionFactoryContext,
    build_environment_run_extension_factory_catalog,
)
from a13n_harness.environment.advanced import create_environment_runtime

catalog = build_environment_run_extension_factory_catalog(
    extension_keys=("acme.workspace-marker",),
)
extension = catalog.create_extension(
    EnvironmentRunExtensionFactoryContext(
        extension_key="acme.workspace-marker",
        extension_id="workspace-marker-primary",
        configuration={"marker_path": "/workspace/.active-run"},
    )
)
environment_runtime = create_environment_runtime(
    mounts=mounts,
    default_mount="workspace",
    extensions=(extension,),
)
bindings = RunBindings.embedded(environment=environment_runtime)
result = await executable.run("Use the prepared workspace", bindings=bindings)

extension_key chooses one installed factory; extension_id identifies one concrete aggregate instance and must be unique within that runtime. Catalog construction imports only explicitly selected keys. Passing a concrete extension directly skips metadata discovery entirely.

Runnable Example

The integration package example contains Host-authorized custom Capability selection, wheel-ready middleware and Environment extension entry points, direct-code composition, YAML selection, public Harness execution, per-Run isolation, and offline tests.

On this page