Middleware¶
Middleware is the most direct way to add behavior: an async function attached to hook points that answers each event with an outcome. Guardrails, validation rules and routers are all installed as middleware; your own middleware runs on the same bus, under the same ordering rules (The hook bus).
import harnessweaver as hw
@hw.middleware(hooks=["tool.before"], band="app", fail_mode="closed", timeout="1s")
async def no_weekend_refunds(event: hw.ToolCallEvent, env: hw.Envelope) -> hw.Outcome | None:
"""Refunds asked for at the weekend wait for someone in finance."""
if event.tool.name == "refund_payment" and env.clock.is_weekend() and event.approval is None:
return hw.Suspend(reason="weekend refunds need approval", approver_role="finance")
return None
The decorator¶
| Argument | Meaning |
|---|---|
hooks |
The hook points it attaches to ("tool.before", or hw.HookPoint values) |
fail_mode |
Required: closed turns a raise or timeout into Block (middleware_error, middleware_timeout); open records it and continues |
band |
The default band (app); the spec item decides, and only profiles may use baseline |
timeout |
A duration or seconds; none by default |
name |
Defaults to the function's name, or a class's name in snake case; lowercase words and _ |
config |
For a class middleware: the Pydantic model of its settings |
sample |
Settings for the conformance suite, when config has required fields |
The function must be async. It receives the hook's event and the run's envelope.
Answering¶
| Return | Effect |
|---|---|
None or hw.Continue() |
Proceed unchanged |
hw.Continue(modified=event.model_copy(update={...})) |
Proceed with a changed event; later middleware and the operation see it |
hw.Retry(reason) |
Redo the step: honored at model.after and output.before |
hw.Escalate(reason, to=...) |
Hand the run to a human queue |
hw.Suspend(reason, approver_role=...) |
Pause for approval: at tool.before and output.before in a harness-driven run; it escalates elsewhere |
hw.Block(reason, code=...) |
Refuse; give a stable code so tests, evals and dashboards can match it |
Never raise to control the run: return an outcome. A modified event must be the same type as the one received. Reasons and codes must never contain content (R9).
@hw.middleware(hooks=["input.before"], fail_mode="closed")
async def strip_signatures(event: hw.InputEvent, env: hw.Envelope) -> hw.Outcome | None:
"""Drop e-mail signatures before anything else sees the input."""
if isinstance(event.input, str) and "\n-- \n" in event.input:
return hw.Continue(modified=event.model_copy(update={"input": event.input.split("\n-- \n")[0]}))
return None
Middleware with settings¶
from pydantic import BaseModel, ConfigDict
class CapSettings(BaseModel):
model_config = ConfigDict(extra="forbid")
max_usd: float
@hw.middleware(hooks=["tool.before"], fail_mode="closed", config=CapSettings)
class RefundCap:
"""Blocks refunds above a cap the spec sets."""
def __init__(self, config: CapSettings) -> None:
self.max_usd = config.max_usd
async def __call__(self, event: hw.ToolCallEvent, env: hw.Envelope) -> hw.Outcome | None:
amount = event.call.arguments.get("amount_usd", 0)
if event.tool.name == "refund_payment" and isinstance(amount, int | float) and amount > self.max_usd:
return hw.Block(reason="over the refund cap", code="refund_cap")
return None
In the spec¶
plugins:
local: ["app.harness_plugin"] # or install the plugin package
middleware:
- { use: acme.no_weekend_refunds }
- { id: cap, use: acme.refund_cap, max_usd: 500, timeout: 500ms }
- { use: acme.audit, hooks: [run.end], fail_mode: open } # narrow the hooks; let an audit failure pass
| Key | Meaning |
|---|---|
use |
The middleware's full name |
id |
Names the item for merges and locks (middleware[cap]); default <use>#<n> |
band |
Defaults to the declaring layer's band; only profiles may set baseline |
hooks |
Narrow the hook points; it can't attach to a hook it doesn't declare |
fail_mode, timeout |
Override the declared ones; baseline middleware must stay closed |
enabled |
false switches it off (not allowed on a locked item) |
| other keys | Settings for a class middleware's config |
middleware is a keyed list, so a later layer can change an item by restating its id, and a profile can lock it (middleware[cap]).
Testing middleware¶
Test the outcome and the trace, and the failure path for its fail mode:
async def test_weekend_refunds_wait() -> None:
harness = harness_for_test(spec, model=scripted(tool_call("refund_payment", amount_usd=20), text("ok")),
tools=[FakeTool("refund_payment")], clock=FakeClock(saturday))
result = await harness.run("refund", "refund please", principal="casey")
assert result.status is hw.RunStatus.AWAITING_APPROVAL
assert_trace(result, step("tool.before", source="acme.no_weekend_refunds", outcome="suspend"))
The conformance suite also calls every middleware with a sample event for each hook it declares and checks it answers with an outcome, never by raising (Conformance).
TypeScript¶
defineMiddleware({ name, hooks, fail_mode, band, timeout, handler }), registered through definePlugin (TypeScript).