Companion repository for the article: Before You Add an Agent, Build the Harness.
Article link: coming soon.
A minimal AI decision harness that demonstrates a critical architectural principle:
The LLM may recommend an action, but the system must validate, constrain, route, and audit the final decision.
This version does not call a real LLM. The classifier is mocked on purpose so the harness architecture is visible without API keys, provider-specific code, or model variability.
This project implements a small, deterministic decision pipeline for infrastructure or operational requests. It shows how an organization can safely integrate LLM-based classification without granting the model final authority.
The harness enforces a strict separation of concerns:
LLM classifies.
Schema validates.
Policy constrains.
Router decides.
Audit records.
The central behavior to observe is the override: the LLM may recommend auto_approve, but the harness can still escalate to human_review or reject based on deterministic policy rules.
Request: REQ-001
LLM recommendation: auto_approve
Final decision: human_review
Overrode LLM: true
Audit written to: audits/REQ-001.json
Large language models are probabilistic tools. They can hallucinate, be jailbroken, miss subtle context, or simply recommend actions that violate organizational policy. Treating an LLM as the final authority turns probabilistic output into unaccountable automation.
Instead, this harness treats the LLM as a classifier and recommender only. Its output is:
- Validated against a strict schema (Zod).
- Evaluated by deterministic policy rules.
- Potentially overridden by those rules.
- Logged for accountability.
The harness is a lightweight CLI application that accepts a JSON request file and runs it through a fixed pipeline:
- Load the request JSON.
- Validate the request shape using Zod.
- Classify the request using a mock LLM classifier.
- Validate the classifier output using Zod.
- Apply deterministic policy rules.
- Route to a final decision.
- Write an audit log under
audits/<request_id>.json. - Print the final decision to stdout.
- A production-ready agent platform.
- A workflow engine.
- A policy engine.
- An LLM framework.
- A replacement for human judgment.
It is a small architectural experiment that isolates one pattern:
Deterministic boundaries around probabilistic model output.
This repository is organized around two stages:
| Branch | Purpose |
|---|---|
main |
Demonstrates the harness using a mock classifier. No API keys required. |
with-llm |
Replaces the mock classifier with a real LLM integration while preserving the same validation, policy, routing, and audit boundaries. |
The important comparison is the diff between the branches: adding an LLM changes the classifier implementation, not the decision authority model.
These constraints must remain true as the project evolves:
- The LLM is not the final decision-maker.
- All request inputs must be validated before processing.
- All LLM outputs must be validated before policy evaluation.
- Policy rules must be deterministic and inspectable.
- The final decision must be produced by routing logic, not by the LLM.
- Any override of the LLM recommendation must be explicit.
- Every run must produce an audit record.
- The system should favor safe escalation over unsafe automation.
This README is also context for coding agents working on the repo.
- Do not collapse the harness into a single prompt, model call, or function.
- Preserve the separation of responsibilities:
- classifier
- schema validation
- policy evaluation
- routing
- audit logging
- Future real LLM integration must not bypass Zod validation.
- Future workflow engine integration must not move final decision authority into the LLM.
- Future policy changes should remain deterministic, testable, and auditable.
- Provider-specific LLM code should not leak into policy or routing logic.
agent-harness-lab/
├── examples/
│ ├── low-risk.json
│ ├── medium-risk.json
│ ├── high-risk.json
│ ├── forbidden.json
│ └── ambiguous.json
├── audits/
├── src/
│ ├── index.ts # CLI entry point and orchestration
│ ├── schemas.ts # Zod schemas for runtime validation
│ ├── types.ts # Central TypeScript exports
│ ├── classify-request.ts # Mock LLM classifier
│ ├── policy-check.ts # Deterministic policy engine
│ ├── route-decision.ts # Decision routing logic
│ └── audit-log.ts # Audit log writer
├── package.json
├── tsconfig.json
└── README.md
npm install# Low risk — auto-approved
npm run review examples/low-risk.json
# Medium risk — classifier recommends human review
npm run review examples/medium-risk.json
# High risk — production + admin API
npm run review examples/high-risk.json
# Forbidden — policy forces rejection
npm run review examples/forbidden.json
# Ambiguous — low confidence triggers escalation
npm run review examples/ambiguous.jsonThe current branch uses a mock classifier, so no API keys or external services are required.
npm run typecheck
npm run build| Command | Description |
|---|---|
npm run review <file> |
Run the harness against a JSON request file |
npm run typecheck |
Run strict TypeScript type checking (no emit) |
npm run build |
Compile TypeScript to dist/ |
Request: REQ-001
LLM recommendation: auto_approve
Final decision: human_review
Overrode LLM: true
Audit written to: audits/REQ-001.json
Policy rules are deterministic, independent from the classifier, and organized by severity:
auto_approve < request_more_info < human_review < reject
Each rule may raise the severity, but no rule may lower it. The policy engine evaluates all rules and returns the highest severity required.
| Rule | Trigger | Minimum Decision |
|---|---|---|
production_environment |
environment === "production" |
human_review |
public_exposure |
Text mentions public exposure | human_review |
admin_api |
Text mentions admin API | human_review |
disable_authentication |
Text mentions disabling auth | reject |
secrets_or_credentials |
Text mentions secrets or credentials | human_review |
sensitive_data |
Text mentions customer data, PII, billing, etc. | human_review |
broad_aws_permissions |
Text mentions AdministratorAccess, wildcard, root access | human_review |
low_confidence |
Classifier confidence < 0.75 | request_more_info |
The routeDecision function compares the LLM recommendation against the policy requirement and returns the more severe of the two. If the final decision differs from the LLM recommendation, the overrode_llm flag is set to true.
Every run produces a JSON audit log:
{
"request": {},
"llm_classification": {},
"policy_decision": {
"final_action": "human_review",
"triggered_rules": ["production_environment", "admin_api"],
"overrode_llm": true
},
"audit": {
"timestamp": "2026-01-15T12:00:00.000Z",
"status": "recorded"
}
}This harness is intentionally minimal. It is an architectural experiment, not a framework. Here are natural evolution paths:
Replace classifyRequest with a call to OpenAI, Anthropic, or a local model. The rest of the pipeline remains unchanged because the LLM output is still validated by Zod and constrained by policy.
Introduce a human-in-the-loop step when the final decision is human_review. This could be:
- A Slack notification to an on-call engineer.
- A GitHub issue or pull request review.
- A custom approval dashboard.
For production use, replace the simple CLI script with a durable workflow engine:
- Inngest — event-driven, great for retries and scheduling.
- Restate — durable execution with stateful handlers.
- Temporal — battle-tested for long-running, fault-tolerant workflows.
- LittleHorse — modern workflow-as-code with strong typing.
These engines can pause for human approval, retry failed LLM calls, and make audit writes more reliable through durable execution and idempotency controls.
- Store audit logs in S3 or a database instead of the local filesystem.
- Replace hardcoded policy rules with a dynamic policy engine (e.g., Open Policy Agent).
- Add fine-grained RBAC so different teams have different policy thresholds.
- Track policy rule versions and migrate historical audits.
MIT