Plugins¶
A plugin is any importable Python module that exposes a harnessweaver.Plugin object. It contributes providers, middleware, routing stages, runners, adapters, profiles and templates, all named under the plugin's namespace.
A plugin package¶
acme-harness/
pyproject.toml
src/acme_harness/
__init__.py # exposes `plugin`
guards.py
middleware.py
profiles/baseline.yaml
py.typed
tests/
test_guards.py
# src/acme_harness/__init__.py
"""Acme's HarnessWeaver plugin: guards and middleware under the namespace `acme`."""
import harnessweaver as hw
from acme_harness import guards, middleware
plugin = hw.Plugin(
namespace="acme", # owns every name acme.*
version="1.2.0",
contract=">=1.0,<2", # the contract range it was built against (V5)
providers=[guards.InHousePII], # @hw.provider classes and @hw.decider functions
middleware=[middleware.no_weekend_refunds],
profiles={"baseline": "profiles/baseline.yaml"},
description="Acme's guards and controls",
)
# pyproject.toml
[project]
name = "acme-harness" # any name: there is no naming convention
version = "1.2.0"
requires-python = ">=3.12"
dependencies = ["harnessweaver>=0.1,<1"]
[project.entry-points."harnessweaver.plugins"]
acme = "acme_harness:plugin" # the entry point's name is the namespace
# an app's harness.yaml
guardrails:
input: [{ use: acme.pii, action: redact, mode: log_only }]
middleware:
- { use: acme.no_weekend_refunds }
harnessweaver plugin new acme writes this layout for you, with a sample guard and tests whose uv run pytest also runs the conformance suite.
hw.Plugin¶
| Argument | Meaning |
|---|---|
namespace |
The plugin's namespace: a lowercase letter, then lowercase letters, digits or _. Must match the entry point's name, and can't be a reserved namespace |
version |
The plugin's SemVer version |
contract |
The contract range it supports (default >=1.0,<2) |
providers |
@hw.provider classes and @hw.decider functions |
middleware |
@hw.middleware functions and classes |
runners |
@hw.runner classes |
adapters |
@hw.adapter classes |
profiles |
Profile name to a YAML file in the package |
templates |
Template name to a directory in the package |
description |
Free text, shown by harnessweaver plugin list |
Two ways to load a plugin¶
| Installed package | In-repo code | |
|---|---|---|
| How | An entry point in the harnessweaver.plugins group |
plugins.local: ["app.harness_plugin"] in the spec |
| Namespace | The entry point's name | The plugin's namespace |
| Use for | Plugins shared between apps | An app's own guards and middleware (Example 03) |
Names and namespaces¶
- Every contributed name is
<namespace>.<name>:acme.pii,openai.chat. A bare name in a decorator (name="pii") gets the plugin's namespace; a name in another namespace is refused. This lets the loader find the owning plugin without importing anything. - The core's built-in plugin owns bare names (
direct,session,workflow,memory) and a set of reserved area namespaces (pii,secrets,regex,injection,size,topic,grounded,llm,cel_lite,rules,lexical,semantic,memory,sqlite,scripted, ...), listed inhw.RESERVED_NAMESPACES. Other plugins can't claim them. - Names are lowercase words joined by dots, unique per port:
memorycan be both a state store and an approval channel. - Loading fails (R4) if two installed plugins claim the same namespace (
PluginConflict), or a referenced name resolves to nothing (UnknownProvider).
How loading works¶
flowchart TD
A["Discover<br/>entry-point metadata + plugins.local<br/>(nothing imported yet)"] --> B["Resolve<br/>namespaces the effective spec refers to"]
B -- "unknown name or namespace conflict" --> X1["UnknownProvider / PluginConflict"]
B --> C{"Allowed by the org baseline?<br/>plugins.allow"}
C -- no --> X2["PluginNotAllowed"]
C -- yes --> D["Import only the referenced plugins"]
D --> E{"Contract range matches<br/>harnessweaver/v1?"}
E -- no --> X3["IncompatibleContract"]
E -- yes --> F["Register contributions<br/>in this Harness's registry"]
F --> G["Validate every config<br/>against its Pydantic model"]
G --> H["Modules build providers and<br/>install middleware on hook points"]
Each Harness has its own registry; the process-wide catalog of installed plugins is read-only after discovery (RULES C4). The built-in plugin is always imported from the core and always allowed.
Profiles and templates¶
A plugin can ship profiles, which specs select as profile://<namespace>.<name>@<range>. Only that plugin is imported, plugins.allow applies to it, and its profiles count as org layers (their guards run in the baseline band). This is a good way to distribute an org baseline as a versioned package.
A plugin can ship templates for harnessweaver init -t <namespace>.<template>. A template is a directory copied as is, with the project name put in; there is no template language. Its output should pass its own tests (RULES T6).
Writing plugin code¶
- Import nothing heavy at import time. Import a client library inside the provider's
__init__or methods, so installing the plugin costs nothing until a spec uses it (S8). The conformance suite checks that importing the plugin opens no sockets, writes no files and installs no global OpenTelemetry provider. - Build from config alone. A provider must be constructible from its config model, with no hidden environment reads beyond those its config documents, such as an
api_key_envsetting (RULES C11). - Refuse the network in test mode. A provider that calls out checks
harnessweaver.ports.network_allowed()and refuses whenHARNESSWEAVER_TEST_MODE=1(R14). - Never put content in logs, traces or errors (R9). Log with loguru and call
logger.disable(__name__)in the package's__init__, so it is silent until the app enables it. - Raise taxonomy errors across the boundary (
ProviderError,ToolError,SpecError), never your library's own (C5).
Inspecting installed plugins¶
lists each installed plugin's namespace, distribution, version, contract range and whether it is compatible with this harnessweaver. harnessweaver doctor reports installed plugins that share or claim a reserved namespace.
First-party plugins¶
| Distribution | Namespace | Contributes |
|---|---|---|
harnessweaver-openai |
openai |
openai.chat model provider; instrument() adapter for OpenAI clients |
harnessweaver-anthropic |
anthropic |
anthropic.messages model provider; instrument() adapter for Anthropic clients |
harnessweaver-pydantic-ai |
pydantic_ai |
pydantic_ai.agent runner; wrap() adapter for agents |
harnessweaver-monty |
monty |
monty.sandbox for code-executing tools |
harnessweaver-otel |
otel |
otel.sdk: OpenTelemetry SDK and exporter setup |
harnessweaver-cel |
cel |
cel.policy: full CEL policy engine |
Their source, in python/plugins/, is the best reference for writing your own.
TypeScript plugins¶
JavaScript has no entry points, so TypeScript plugins are registered explicitly: a package exports definePlugin({...}) and the app passes it to load("harness.yaml", { plugins: [acme] }). The same naming, contract and allow-list rules apply. See TypeScript.