Providers and ports¶
A port is a typed interface for one capability, a Protocol in harnessweaver.ports. A provider is a class that satisfies it, plus a Pydantic config model so the loader can validate its settings. Decorate the class with @hw.provider, list it in your plugin, and specs select it with use: <namespace>.<name>.
from pydantic import BaseModel, ConfigDict
import harnessweaver as hw
class InHousePIIConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
endpoint: str
entities: list[str] = ["EMAIL", "PHONE"]
@hw.provider(port="Guardrail", name="acme.pii", config=InHousePIIConfig,
contract=">=1.0,<2", stages=["input", "tool_result", "output"],
deterministic=True, latency_class="fast",
sample={"endpoint": "http://pii.test"})
class InHousePII:
"""Acme's own PII detector, behind the Guardrail port."""
def __init__(self, config: InHousePIIConfig) -> None:
from acme_pii import Client # imported on first use, not at plugin import (S8)
self.client = Client(config.endpoint)
self.entities = config.entities
async def check(self, content: str, env: hw.Envelope) -> hw.GuardResult:
hits = await self.client.scan(content, self.entities)
if not hits:
return hw.GuardResult.allow()
findings = tuple(hw.Finding(type=h.entity, start=h.start, end=h.end, private=True) for h in hits)
return hw.GuardResult.redact(self.client.mask(content, hits), findings=findings)
guardrails:
input:
- { use: acme.pii, endpoint: "http://pii.internal", entities: [EMAIL], mode: log_only }
@hw.provider¶
| Argument | Meaning |
|---|---|
port |
The port it implements (below) |
name |
<namespace>.<name>, or a bare name that gets the plugin's namespace |
config |
The Pydantic model the spec's settings are validated against (use extra="forbid"); the class is built as Cls(config). Omit it for a provider without settings |
contract |
The contract range it supports (default >=1.0,<2) |
deterministic |
Whether the same input always gives the same answer. A PolicyEngine must be deterministic (RULES R3); the conformance suite checks it |
latency_class |
fast (p95 within 25 ms), medium (250 ms, the default) or slow (5 s), checked by the conformance suite |
stages |
For guardrails: the stages it may run at; a spec that puts it elsewhere fails to load |
sample |
Settings the conformance suite builds it with, required when config has required fields |
Settings arrive validated: the spec item's keys that HarnessWeaver doesn't own (everything but id, use, action, mode, ... for a guard) become the config model. A misspelled setting fails when the harness loads (R4).
The ports¶
| Port | Methods | Selected by |
|---|---|---|
Guardrail |
async check(content: str, env: Envelope) -> GuardResult |
guardrails.<stage>[].use, tools.judge.use |
PolicyEngine |
decide(request: PolicyRequest) -> PolicyDecision |
policy.use |
Decider |
async __call__(request: DecisionRequest) -> Decision |
routing.<router>.cascade[].use (or @hw.decider) |
Embedder |
embed(texts: Sequence[str]) -> list[list[float]] |
a semantic stage's encoder |
ContextProvider |
async fetch(query: str, env: Envelope, *, limit: int | None) -> list[ContextItem] |
context.<source>.use |
Memory |
put(namespace, key, value, *, expires_at), get(namespace, key, *, now), delete(namespace, key), entries(namespace, *, now) |
memory.use |
ModelProvider |
async generate(request: ModelRequest, env: Envelope) -> ModelResponse |
models.provider |
StreamingModelProvider |
generate(...), and stream(request, env) yielding StreamChunks, then the final ModelResponse |
models.provider |
Evaluator |
grade(sample: EvalSample) -> Grade |
a grader name in an eval dataset |
ApprovalChannel |
async notify(request: ApprovalRequest, env: Envelope) -> None |
approvals.use |
StateStore |
runs, checkpoints, approval and idempotency claims, events, recordings, online samples, aclose() |
state.use |
Sandbox |
run(code: str, inputs: Mapping, limits: SandboxLimits) -> SandboxResult |
a code tool's sandbox |
Telemetry |
configure(*, service_name, exporter, endpoint) |
observability.configure_sdk |
The method signatures are in harnessweaver.ports, typed; check whether each is sync or async there. A few notes per port:
- Guardrail. Return
GuardResult.allow(),redact(content, findings=...),block(reason, findings=...),repair(...),escalate(...)orrequire_approval(...). Mark personal data findingsprivate=True, so the run'sprivate_seenlabel is set (S3). Reasons and findings name types, never the content (R9). See Guardrails. - ContextProvider. Return only what the envelope's tenant and principal may see; the harness re-checks ACLs anyway.
- ModelProvider. Take and return only neutral contracts (R7); put a provider's own per-turn data in
ModelResponse.extensions; turn client errors intoProviderError; refuse the network in test mode (harnessweaver.ports.network_allowed()); report cost incost_usdwhen the API gives it; build clients with retries off when a gateway owns them (R5). - PolicyEngine. Combine
policy.rulesexactly asdecide_by_rulesdoes (Policy); declaredeterministic=True. - Evaluator. Score 1 or 0 for a deterministic grader. Give the config model a
SHORTHAND: ClassVar[str | None]naming the field a single value fills, so{field_equals: total}works like{field_equals: {field: total}}. - StateStore. Pass the conformance suite's round-trip contract;
claim_idempotencyandclaim_approvalmust be atomic, across processes for a store they share.
Functions as providers¶
A routing stage can be a plain async function with @hw.decider (Deciders). Everything else is a class.
Registering¶
List providers in your plugin's providers=[...]. Their names must sit in the plugin's namespace (Plugins). Then:
checks every contribution (Conformance).
Example: a guard¶
A guard is a Guardrail provider: a class with a config model and an async check(content, env) that returns a GuardResult.
from pydantic import BaseModel, ConfigDict
import harnessweaver as hw
class AccountIdsConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
prefix: str = "ACCT-"
@hw.provider(port="Guardrail", name="acme.account_ids", config=AccountIdsConfig,
stages=["input", "tool_result", "output"], deterministic=True, latency_class="fast")
class AccountIds:
"""Redacts Acme account numbers."""
def __init__(self, config: AccountIdsConfig) -> None:
self.prefix = config.prefix
async def check(self, content: str, env: hw.Envelope) -> hw.GuardResult:
if self.prefix not in content:
return hw.GuardResult.allow()
redacted = content.replace(self.prefix, "[ACCOUNT]-")
return hw.GuardResult.redact(redacted, findings=(hw.Finding(type="account_id"),))
GuardResult has allow(), redact(content, findings=..., reason=...), block(reason, findings=...), repair(...), escalate(...) and require_approval(...). A Finding has a type, optional start, end and score, and private: true when it is personal data. Reasons and findings name types, never the content (R9). Register the class in your plugin and use it as use: acme.account_ids. The conformance suite checks it for determinism, timeouts, latency and leaks (Conformance). For TypeScript, see defineProvider({ port: "Guardrail", ... }) in TypeScript.