Skip to content

Providers

Every built-in and first-party provider, with its settings. A provider is chosen with use: (or provider: in models), and its settings sit next to that key. They are validated against the provider's config model when the harness loads, so a misspelled setting fails (RULES R4).

Every provider declares whether it is deterministic. Probabilistic components may route a decision or make it stricter, never grant anything (RULES R3).

Deterministic Probabilistic
pii.regex, secrets.scan, injection.heuristic, regex.deny, size.limit, topic.deny, grounded.citations, cel_lite, rules, lexical, semantic (with the hashing embedder), the deterministic graders llm.judge, the llm routing stage, the llm_judge grader

Models

The models section picks one provider with provider: (Models and environments).

scripted

A deterministic model for tests and offline runs (RULES R14). It ignores settings meant for a real provider (such as base_url), so an overlay can switch to it without repeating the section.

models:
  provider: scripted
  rules:                                    # checked first, against the last message
    - { contains: "refund", reply: { tool_calls: [{ name: refund, arguments: { amount: 5 } }] } }
    - { role: tool, reply: { text: "Refund done." } }
  replies:                                  # then used in order
    - { output: { vendor: "ACME", total: 12.5 } }
  repeat_last: true
Setting Meaning
rules Each has a reply and matchers on the last message: contains (case-insensitive), matches (a regex), role; and times, after which it stops matching
replies Used in order when no rule matches. When they run out, the last repeats, unless repeat_last: false (then the call fails)

A reply has text, output (structured output, also sent as JSON text), tool_calls ({name, arguments, id}), usage (default: estimated from the text, one token per four characters), cost_usd, model, chunks (how to stream the text), finish_reason, and fail: true to raise a ProviderError. In tests, build one with harnessweaver.testing.scripted(...) (Testing).

openai.chat

From harnessweaver-openai: Chat Completions on any OpenAI-compatible endpoint (LiteLLM Proxy, OpenAI, vLLM, Ollama).

Setting Default Meaning
base_url the SDK's default The endpoint, usually the gateway
api_key_env OPENAI_API_KEY The environment variable with the key. Keys never go in the spec (S1)
timeout 60s Per request
max_retries 0 SDK retries. The gateway retries (R5)
max_tokens_param max_tokens Or max_completion_tokens
cost_header x-litellm-response-cost The gateway's reported cost, used before models.prices
default_headers none Extra headers on every request
hold_stream false Deliver a streamed answer's chunks only once it has finished, so nothing from a declined answer reaches the app. Output is no longer live
block_empty_answers false Treat an answer with no text and no tool calls as declined

Output schemas are sent as response_format (non-strict JSON Schema). Streaming includes usage.

A request the model declines ends the run blocked with the code model_refusal, and its tokens and cost still count. It is recognized by the message's refusal field (LiteLLM moves it to provider_specific_fields, which is also read) or the finish reason content_filter, which is how LiteLLM reports a Claude refusal. So an answer a content filter cut short ends the same way, and its partial text is dropped.

Three cases need a setting, because the signal is late or missing:

Case What happens by default What to set
The signal arrives after text has been streamed The run ends blocked, but those chunks have already been delivered hold_stream: true
A streamed refusal through LiteLLM, which drops the field (seen on 1.106.6) An empty answer that completes normally block_empty_answers: true. Off by default, because a model may also end a tool loop with nothing to add
An endpoint that sends a refusal as ordinary text An answer like any other A regex.deny output guard on the wording you see. It starts in shadow mode (RULES R8) and ends the run with the code guard_blocked. Guards don't run on empty text
guardrails:
  output:
    - id: refusal-text
      use: regex.deny
      patterns: ["^i'm sorry, but i can't"]
      ignore_case: true
      mode: log_only

anthropic.messages

From harnessweaver-anthropic: the Claude Messages API, called directly or through a gateway that serves it.

models:
  provider: anthropic.messages
  default: strong
  aliases: { strong: claude-opus-5-5, fast: claude-haiku-5-5 }
  effort: high
Setting Default Meaning
model claude-opus-5-5 When a request names no model
max_tokens / stream_max_tokens 16000 / 64000 When a request sets none
thinking adaptive Or none to omit the parameter
effort the model's default low, medium, high, xhigh, max
fallbacks default Server-side refusal fallbacks; none turns them off
structured_outputs false Send output schemas as the API's structured output format (they must meet its rules); otherwise the schema goes in the system prompt
sampling false Forward temperature (older models only); current models reject sampling parameters
timeout 600s Per request
max_retries 2 SDK retries. Set 0 behind a gateway, which then owns retries (R5)
cost_header x-litellm-response-cost A gateway's reported cost, used before models.prices
base_url, api_key_env SDK defaults Client settings. Credentials follow the SDK's own resolution unless api_key_env names a variable
hold_stream false Deliver a streamed answer's chunks only once it has finished, so nothing from a declined answer reaches the app. Output is no longer live

A request the model still declines after fallbacks ends the run blocked with the code model_refusal, and its tokens still count. Each answer's content blocks, thinking included, are replayed unchanged on the next request of a tool loop, so reasoning carries over.

Choosing a model provider

openai.chat is the one route to every model: point it at a gateway, and the gateway reaches the rest, Claude included. But it speaks only Chat Completions, so what that API doesn't carry is lost. To use what is specific to Claude, use anthropic.messages.

openai.chat anthropic.messages
Reaches Any model behind an OpenAI-compatible endpoint Claude, directly or through a gateway that serves the Messages API
Thinking Not requested, and not replayed between turns Adaptive, and replayed between turns of a tool loop
effort, thinking, fallbacks Not settings of this provider: naming one fails loading (RULES R4) Settings of this provider
A request the model declines The run ends blocked with the code model_refusal, with no fallback and no category. Through LiteLLM it can't be told apart from a content filter Re-run on the fallback model, then the run ends blocked with the code model_refusal and the refusal's category
temperature Sent when a request sets it Sent only with sampling: true
SDK retries by default 0: the gateway owns them (RULES R5) 2: set 0 behind a gateway

Behind a gateway, anthropic.messages takes base_url and max_retries: 0, and fallbacks: none if the gateway rejects beta parameters.

harness.instrument() is a separate choice. It picks its adapter from the client the app passes, not from models.provider, so each SDK needs its own plugin (Govern an existing client).

Guards

Guards are listed per stage under guardrails (Guardrails). These are each guard's own settings. The keys every guard shares (id, use, action, mode, where, parallel, fail_mode, timeout, enabled) are in the section reference. The built-in patterns are part of the contract, in regex syntax Python and TypeScript share.

pii.regex

Finds e-mail addresses, phone numbers, card numbers (Luhn-checked), US social security numbers and IP addresses, and redacts them. Its findings are marked private, so a run where it finds PII is marked private wherever the guard runs (the trifecta check reads this).

Setting Default Meaning
entities all Any of EMAIL, CREDIT_CARD, SSN, PHONE, IP_ADDRESS
replacement [{entity}] {entity} is replaced by the entity name, so ann@example.com becomes [EMAIL]

Proposes redact. Stages: all.

secrets.scan

Finds credentials: private keys, AWS access keys, GitHub, Slack, Anthropic and OpenAI keys, Google API keys, JWTs, and credential assignments such as password=....

Setting Default Meaning
kinds all Any of private_key, aws_access_key, github_token, slack_token, anthropic_key, openai_key, google_api_key, jwt, credential_assignment
replacement [SECRET] Used with action: redact

Proposes block. Stages: all.

injection.heuristic

Scores phrases typical of prompt injection (ignore previous instructions, requests to reveal the system prompt, role overrides, fake system messages, exfiltration requests) and flags content whose score reaches the threshold. It is cheap and easy to evade: pair it with the trifecta check, untrusted labels and approvals, and add a model-based detector when the stakes need one.

Setting Default Meaning
threshold 0.5 The score that flags content

Proposes block. Stages: input, context, tool_result.

regex.deny

Flags content matching any of your patterns.

Setting Default Meaning
patterns required Regular expressions (the syntax Python and JavaScript share)
ignore_case false Match regardless of case
replacement [REDACTED] Used with action: redact

Proposes block. Stages: all.

size.limit

Flags content over a size. Its redaction truncates.

Setting Default Meaning
max_chars none Characters (code points)
max_bytes none UTF-8 bytes

Proposes block. Stages: all.

topic.deny

Flags content that mentions denied topics, by whole-word keywords, ignoring case.

Setting Default Meaning
topics required A map of topic name to keywords: {legal: [lawsuit, attorney]}
replacement [REDACTED] Used with action: redact

Proposes block. Stages: input, output.

grounded.citations

Checks that the output's sentences (ending at ., ! or ? and a space, or a line break) of at least min_words words cite a context item in square brackets, like [help-refunds], and that each citation names an item the run actually retrieved. Proposes repair (the model is asked to cite). Set action: escalate to send uncited answers to a person instead.

Setting Default Meaning
min_supported 0.9 The share of sentences that must cite
min_words 4 Shorter sentences need no citation
require_known true Citations must name items the run kept
pattern \[([A-Za-z0-9_.:#/-]+)\] How a citation looks; group 1 is the item id

Stages: output.

llm.judge

Asks a model whether the content meets your criteria. The call is governed and budgeted like any other model call, and the judge's own explanation isn't kept (R9). It is probabilistic, so it can only make a decision stricter (R3).

Setting Default Meaning
criteria required What acceptable content looks like
model models.default The model alias for the judge

Proposes block when the content fails. Stages: input, output.

Routing stages

Stages of a router's cascade (Routing). Every stage also takes id and threshold (section reference).

Stage Settings
rules rules: a list of {when, choice}; when sees text, state, options and router. Or file: a YAML file with a rules list, relative to the spec. A rule that can't be evaluated doesn't match
lexical utterances: a map of option to example texts, or file: a YAML file with an utterances map. Each option scores its closest example by the cosine of token counts; ties go to the option listed first; a text that shares no token with any example abstains
semantic utterances or file, as for lexical, and encoder (default semantic.hashing, an Embedder provider). semantic.hashing hashes words and character trigrams (32-bit FNV-1a) into a vector of dims (default 256): dependency-free and deterministic
llm model (default models.default) and instructions. Asks the model to pick an option with a confidence; the call is governed and budgeted, and the explanation isn't kept. The confidence is uncalibrated: tune the threshold on labelled examples

Context sources

Sources under context (Retrieval and memory). Every source also takes label, max_items, max_tokens and require_acl (section reference).

Provider What it does Settings
memory Documents held in memory, searched lexically (token cosine) documents: a list of {id, content, acl, metadata}; or file: a YAML file with a documents list, relative to the spec
passthrough Fetches nothing; governs the items your code passes to s.context(name).check(items) none

The memory section's store (use: memory) keeps key-value entries in the process, namespaced by tenant, principal and task. Vector stores and search services (pgvector, OpenSearch, Qdrant) are planned as plugins.

Approval channels

The channel approvals.use names is notified of each request once it and the run's checkpoint are saved (Approvals).

Channel Does Settings
memory Keeps every request (the default), for tests and apps that poll none
callback Calls your function, sync or async, with each ApprovalRequest. It must not answer in-line handler: package.module:function
console Writes one line per request, for local development stream: stderr (default) or stdout
webhook POSTs each request as JSON url; timeout (default 10s); headers_env: header name to environment variable (S1). Refuses the network in test mode

A channel that fails is traced (approval_notify_failed) and the run keeps waiting: the request is already saved. Slack, e-mail and an approval page are planned.

State stores

Store Keeps Settings
memory Everything in the process (the default) none
sqlite Everything in a SQLite file path: default .harness/state.db, relative to the working directory; :memory: keeps it in memory

Any StateStore provider works, and the conformance suite checks it with a round-trip contract, including that only one answer can claim a pending approval. Postgres and Redis stores are planned.

Policy engines

Engine From Notes
cel_lite The core The default; the same in Python and TypeScript
cel.policy harnessweaver-cel Full CEL through cel-python. It doesn't mix ints and doubles (1 + 0.5 is an error), and such an error makes the rule unevaluable, so deny and require_approval rules still apply

Both pass the shared governor fixtures. Any PolicyEngine provider must declare deterministic=True, or loading fails (R3).

Graders

Graders in eval datasets (Evals). A grader is written as its name (schema_valid) or as {name: settings}, and a single value fills its main setting: {field_equals: intent} is {field_equals: {field: intent}}. The deterministic ones score 1 or 0 and behave identically in Python and TypeScript (fixtures suite evals).

Grader Main setting Other settings Passes when
exact - - The output equals expect (JSON equality: numbers by value, maps in any order, never a boolean as a number)
field_equals field - The output's field (a dotted path such as line_items.1.amount) equals the same field of expect
numeric_close field tolerance (default 1e-6) The number at field (or the whole output) is within tolerance of expect's
contains text (one or a list) ignore_case The output's text contains every listed text
regex pattern - The pattern matches somewhere in the output's text
citation_present ids (one or a list) pattern The output cites every listed id ([kb-1]), or something, when no ids are listed
schema_valid - - The run completed and its output matches the task's output schema
guard_fired guard (ids or full names) fired (default true), action Each listed guard fired in the run, or would have in shadow mode; fired: false for cases that must stay quiet
status status (one or a list) - The run ended with one of the listed statuses
llm_judge rubric model, threshold (default 0.5) A model scores the output from 0 to 1 against the rubric, given the input, at or above threshold

"The output's text" is the output itself when it is a string, and otherwise its canonical JSON: sorted keys, no spaces, non-ASCII kept, integral numbers without a fraction. In results, each grader is labelled name, name:value (for a text, number or boolean setting) or name#n (for a list or map setting). llm_judge grades inside a session, so its calls are governed and budgeted, and its reason isn't kept (R9). A grader that raises fails its case, not the suite.

Sandboxes

Sandbox From Runs
monty.sandbox (monty for short) harnessweaver-monty Code-executing tools (transport: code) away from the host process, with no host files, network or environment, a fresh worker per call (The tool governor)

Telemetry

Provider From Does
otel.sdk harnessweaver-otel Installs the OpenTelemetry SDK and an exporter when observability.configure_sdk is true (Observability)