Configuration
The harness is configured via harness.yaml in your project root. Create one with kcp-harness init.
Full Reference
version: "1.0"
governance:
domains:
- manifest: "./knowledge.yaml" # Path to knowledge manifest
paths: # File paths governed by this manifest
- "docs/"
- "src/models/"
urls: # URL prefixes governed by this manifest
- "https://docs.example.com/"
tools: # Custom tool names to govern
- "custom_knowledge_tool"
policy:
fail_closed: true # Block access when governance can't verify (default: true)
audit_all: true # Log pass-through calls too (default: true)
max_units: 5 # Maximum knowledge units per plan
strict: false # Strict mode — reject low-relevance units
budget: # Monetary budget ceiling
amount: 1.00
currency: USDC
context_budget: 50000 # Token budget ceiling
env: prod # Environment (affects temporal gates)
confidence: # Post-synthesis confidence gate (harness_assess)
threshold: 0.7 # Pass/fail line, 0..1 — callers may tighten, never loosen
severity: critical # Label recorded on verdicts
route_to_role: account-owner # Route failed verdicts to this approval role
expires_after: 72h # TTL for routed tickets
policy_ref: POL-9.1 # Policy citation carried as ticket evidence
approvals: # Human-approval gates
provider: file # Ticket store: file (persisted) or memory
dir: .kcp-harness/approvals
grant_max: 24h # Hard cap on any approval's validity (default 24h)
require_signed_resolutions: false # true: an unsigned/invalid resolution fails closed, at
# resolve() AND when the store is read (recommended)
require_args_binding: false # true: a ticket not bound to the call's arguments is never a
# grant (recommended; default false for pre-upgrade tickets)
max_reviewed_at_skew: 5m # How far ahead of the store's clock a reviewedAt may be (default 5m)
trusted_keys: # Optional. Reviewer public keys (paths or inline PEM/base64/
- ./keys/kari.pub # hex). When set, a signature must verify against one of these
# to bind it to a named identity; when omitted, the signature's
# own embedded key is used (integrity only, not identity).
rules:
- match: # Absent criteria match everything; present ones AND together
tools: [Write, Edit]
paths: [records/]
required_role: account-owner
expires_after: 72h # Unresolved tickets expire (fail-closed)
policy_ref: POL-7.2
downstream:
- name: "filesystem" # Downstream MCP server name
command: "npx" # Command to launch
args: # Arguments
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "."
audit:
path: ".kcp-harness/audit.jsonl" # Audit log pathGoverned Domains
Each domain maps a knowledge.yaml manifest to the paths, URLs, and tools it governs. When a tool call targets a governed path, the harness routes it through the manifest's 14-gate cascade.
You can have multiple domains:
governance:
domains:
- manifest: "./docs/knowledge.yaml"
paths: ["docs/"]
- manifest: "./api/knowledge.yaml"
paths: ["src/api/"]
urls: ["https://api.example.com/"]Policy
| Field | Type | Default | Description |
|---|---|---|---|
fail_closed | boolean | true | Block ungoverned access to governed paths |
audit_all | boolean | true | Log pass-through (non-governed) tool calls |
max_units | number | 5 | Maximum knowledge units per plan |
strict | boolean | false | Reject units below relevance threshold |
budget | object | — | Monetary budget ceiling (amount + currency) |
context_budget | number | — | Token budget ceiling |
env | string | — | Environment name (affects temporal gates) |
Human-Approval Gates
governance.approvals holds calls for a named human — no matter what the automated governance paths would decide. See Governance Model for the state machine and invariants.
| Field | Type | Description |
|---|---|---|
provider | file | memory | Ticket store. file (default) persists to dir and survives restarts |
dir | string | Store directory (default .kcp-harness/approvals) |
rules[].match.tools | string[] | Tool names the rule applies to (absent = all) |
rules[].match.paths | string[] | Governed path prefixes (absent = all) |
rules[].required_role | string | Role that must approve — required |
rules[].expires_after | duration | Ticket TTL (30m, 72h, 7d); expired = fail-closed |
rules[].policy_ref | string | Policy citation carried as ticket evidence |
rules[].grant | once | session | duration | What an approval authorises: the first matching call (once), the opening session (session, default), or a duration (15m, 4h, 1d, same session). Always capped by grant_max. A typo is a config error |
grant_max | duration | Hard maximum validity of any approval, measured from the moment the store recorded it, not from the reviewer-supplied reviewedAt (default 24h) |
require_signed_resolutions | bool | Require a valid ed25519 signature on every resolution (default false). Enforced in resolve() and on every read of the store: an unsigned or unverifiable resolution line is ignored, the ticket stays pending, and the call is denied (grant_denied / invalid). Recommended true, with trusted_keys |
trusted_keys | string[] | Reviewer public keys (paths or inline PEM/base64/hex) a signature must verify against. Without them the signature's own embedded key is used (integrity, not identity) |
require_args_binding | bool | Never honour a grant from a ticket that carries no argsDigest (default false, for tickets written before binding was explicit; recommended true). Exempt: the harness_assess ticket, which has no call arguments. See Argument binding |
max_reviewed_at_skew | duration | How far ahead of the store's clock a resolution's reviewedAt may be before resolve() refuses it and a read ignores it (default 5m) |
An approval is a bounded grant, never a standing permission — see Approval grants. What the ticket store does and does not protect is stated in Ticket store trust model.
A config must govern something
kcp-harness check and kcp-harness serve fail (exit 1) when:
governance.approvals.rulesexist butgovernance.domainsis empty, orgovernance.domainshas entries but none names apaths,urls,toolsorskillsentry.
Both leave every call outside governance, and an ungoverned call is passed through, so a mangled file would otherwise be silently open. check warns (and still exits 0) when no domain is configured at all, or when approvals are configured but no rule's match.paths overlaps any domain path (no ticket could ever open). A config whose root, governance, or governance.domains has the wrong YAML type (a list, a scalar) is a parse error rather than an empty configuration.
Approval requirements are org policy, not knowledge provenance — they live here in harness.yaml, never in the (signed) knowledge.yaml.
Confidence Gate
governance.confidence configures the harness_assess tool.
| Field | Type | Description |
|---|---|---|
threshold | number | Pass/fail line, 0..1. A caller-supplied threshold can tighten this but never loosen it |
severity | string | Label recorded on verdicts (e.g. critical) |
route_to_role | string | When set (and approvals are configured), failed verdicts open an approval ticket for this role |
expires_after | duration | TTL for routed tickets |
policy_ref | string | Policy citation carried as ticket evidence |
Conformance Routing
governance.conformance routes holds opened by the procedural conformance gate (an active skill's action_scope violation) — independent of the confidence gate above, since "out of scope" and "low confidence" are different events that may need different reviewers or policy citations.
| Field | Type | Description |
|---|---|---|
route_to_role | string | Route conformance holds to this approval role (requires governance.approvals) |
expires_after | duration | TTL for routed conformance tickets |
policy_ref | string | Policy citation carried as conformance ticket evidence |
When governance.conformance is absent, a conformance hold falls back to governance.confidence's routing fields if configured, then to a hardcoded default role (governance-reviewer) with no policy citation.
Audit
The audit log is an append-only JSONL file. Each line is a self-contained event. See Audit Log for the full event schema.
Environment Variables
| Variable | Description |
|---|---|
KCP_HARNESS_CONFIG | Override config file path (default: harness.yaml) |
KCP_HARNESS_AUDIT | Override audit log path |
ANTHROPIC_API_KEY | Required if kcp-agent needs Claude API access |