
Harness Engineering: building an ops agent with AgentCore & Strands
Working with AI as a Software Engineer, Data Engineer or any other role can help us a lot. Having tools can make us quicker, AI is good in summarization, finding where data lives in the enterprise, who owns it, maybe even harmonize data between multiple legacy systems, tasks that for sure require a lot of hours if done manually. But how can we fix the important aspect of trusting what generative AI delivers back to us as an answer?
What is Harness Engineering?
One approach that has grown fast in popularity is agents combined with something called harness engineering.
So what is harness engineering, really? The goal for us here is to learn the basics of yet another concept in AI but also build a proof of concept for correctness and audit trace.
The AI model is the CPU, not the full computer
A language model on its own is like a bare CPU lying on a desk. Has a lot of computational power, but by itself, unable to do a single useful thing for you. Elements of AI makes the same point about computers in general: a processor only becomes a computer inside a system. Memory that holds what it is working on. Input and output that connect it to the world. An operating system that decides what it is allowed to do.
The harness is that system, built around the model. Harness engineering is the discipline of designing it - so that raw capability becomes something you can trust in your daily work.
The pillars of harness engineering
When you use an agentic tool like Kiro, most of what impresses me is not the model. It is the harness. Concretely, a harness is made up of five things:
Tools. The agent's connection to reality: the ability to run a command, query a database, call an API, search a metadata catalog. An AI model without tools can only tell you what it remembers from training. A model with tools can go and look. This single difference is the largest hallucination reducer that exists - most hallucinations are not lies, they are answers given from assumptions, the closest neighbor, bayes thereom, or any other algorithm the AI was built on.
Context management. What the agent gets to see, and when. Instruction files, skills, retrieved documents, the schema of the system it is working in. A well-engineered harness feeds the agent current, authoritative context instead of letting it lean on stale training data. This is where files like AGENTS.md and reusable skills are defined.
Guardrails. The rules the agent cannot break regardless of what it generates: which directories it may write to, which actions require approval, which claims require evidence. Guardrails are enforced by the harness, not requested politely in a prompt. There is a world of difference between "please do not delete files" and an execution environment where the delete call does not exist.
Output contracts. Schemas that the agent's answers must conform to. When an agent must return a defined structure, it can be a certain schema, types or format. Downstream systems can consume the output safely, and you can compare answers mechanically.
Observability. A record of what the agent actually did: which tools it called, in what order, with what results. This trajectory is the harness's audit log, and it is the key to the trust question. It is a way to understand how agents came to choices.
TLDR; the harness lets AI retrieve the current answer from an authoritative source.
What is the real example here?
Imagine you're working for a company where someone asks your AI tool "where does our revenue data from October 2025 live and what does it tell us?" - with no harness the answer will confidently say that a table exists in a completely fabricated place. If you apply harness to a model with a lookup tool with guardrails the answers will be tied to a certain warehouse or finding nothing and output says so.
What are we building?
As a platform engineer in a big company a frequent question could be "why did our Bedrock bill spike?" "what can we save most on in terms of infrastructure?"
We are building a harnessed coding agent to build a harnessed ops agent that focuses on exactly this. The answers our engine will produce are audit logs with references to AWS API calls.
Prerequisites
- Node.js 20+ (also npm)
- Terraform 1.5+
- Docker with buildx (AgentCore Runtime containers must be linux/arm64, if you're on an x86 machine you'll also need QEMU)
- AWS CLI v2, signed in to an account with Amazon Bedrock model access enabled (I use Claude via an EU inference profile in
eu-central-1)
Quick check by running
node -v && terraform -v && docker ps && aws sts get-caller-identityThe chain stops at the first missing piece, which tells you exactly what to install. On Arch Linux (my machine), the gaps were filled with:
1
2
3
sudo pacman -S terraform docker docker-buildx qemu-user-static qemu-user-static-binfmt aws-cli-v2
sudo systemctl enable --now docker
sudo usermod -aG docker $USER # re-login, or newgrp docker for this shellFor macOS:
brew tap hashicorp/tap && brew install hashicorp/tap/terraformFor AWS login
1
2
aws configure set region eu-central-1
aws loginWhen the command
aws sts get-caller-identity returns your account JSON, you're ready. Create the project:1
2
3
4
5
6
mkdir grounded-ops-agent && cd grounded-ops-agent
npm init -y && npm pkg set type=module
npm install @strands-agents/sdk zod express \
@aws-sdk/client-s3 \
@aws-sdk/client-cost-explorer
npm install -D typescript tsx @types/express @types/nodeRuntime
Amazon Bedrock AgentCore gives us the platform for deployment. It is a container based runtime. AgentCore has all ingredients for enterprise-ready harness engineering (Identity, policies, storage, observability, evaluations).
Framework
Strands Agents is AWS's open-source, model-agnostic agent framework. When in early release Python was the main choice. We will use Typescript. Why? Mainly because we want to do structured output with Zod schemas with validation retry on fail. The evidence contract is therefore enforced by the SDK. Fun fact: the TypeScript SDK recently moved into a monorepo literally named
harness-sdk.The evidence contract
This will be the agent's grounded answer instead of free text. The answer can't be verified unless we have evidence reference.
What is Zod?
It is a TypeScript-first schema validation with static type inference. With Zod you can define schemas you can use to validate data, from a simple
string to a complex nested object. 1
2
3
4
5
export const ClaimSchema = z.object({
statement: z.string(),
status: z.enum(['verified', 'not_found']), // The agent will keep track of either verified or not found
evidenceIds: z.array(z.string()).default([]),
})Every time the code runs
verified responses must point at evidence that actually exists in the output.1
2
3
4
5
6
7
8
9
.superRefine((answer, ctx) => {
const known = new Set(answer.evidence.map((e) => e.evidenceId))
answer.claims.forEach((c, i) => {
if (c.status === 'verified' &&
(c.evidenceIds.length === 0 || c.evidenceIds.some((id) => !known.has(id))))
ctx.addIssue({ code: 'custom', path: ['claims', i],
message: `Claim lacks real evidence: "${c.statement}"` })
})
})If answer.claims is marked as
verified, it must reference real evidence that exists in answer.evidence.
Overview of evidence based ops agent
Storing the version enterprise data
The enterprise data intelligence need to be stored somewhere where the agent can correctly query. What we want to store is a model where the agent can reason over. I considered a few storage option. But came to S3, much because of versioning. Every read gives a VersionId, so an audit log is created that lets us trace every claim back to an exact version and time
The EDM has four entity types:
owner: business area where each with a data owner and data stewardteams: with their domain (such as finance), Slack channel and contact person.
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "payments-api",
"domain": "payments",
"owner": "team-atlas",
"account": "workloads",
"tier": 1,
"lifecycle": "production",
"dataClassification": "pii",
"dependsOn": ["ledger-service", "identity-service"],
"datastores": ["payments-events"],
"runbook": "https://wiki.example.com/runbooks/payments-api",
"slo": { "availability": "99.95%", "latencyP99Ms": 300 }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
resource "aws_s3_bucket" "edm" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_versioning" "edm" {
bucket = aws_s3_bucket.edm.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_object" "edm" {
bucket = aws_s3_bucket.edm.id
key = "enterprise-data-model.json"
source = "${path.module}/../src/edm.example.json"
etag = filemd5("${path.module}/../src/edm.example.json")
}The full implementation of the IAM you will see in the deploy section. In an enterprise the JSON would probably have its source from a CMDB or Backstage. We're going to skip the pipeline CI/CD so you can deploy the S3 (assuming you have added the above code in an infra directory) with:
1
2
3
cd infra
terraform init
terraform applyNext we are creating the edmLookup method. This is the agents connection to reality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const edmLookup = tool({
name: 'edm_lookup',
description: 'Enterprise Data Model is the single source of truth for domains, teams, services, datastores' +
'For relationship questions, make multiple lookups and walk the references.',
inputSchema: z.object({
entityType: z.enum(['domain', 'team', 'service', 'datastore']),
name: z.string().optional(), // omit to list all
}),
callback: async ({ entityType, name }) => {
const { edm, etag, versionId } = await loadEdm() // S3 GetObject
return JSON.stringify({
results: queryEdm(edm, entityType, name),
source: { api: 's3:GetObject', etag, versionId, entityType, name },
})
},
})Cost explorer querying
So in order for the agent to be able to use the context we just created through the edmLookup we need the method costLookup. The costLookup enables us to fetch data from Cost Explorer with daily or monthly granularity. It is also able to check for services. This method is deterministic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const costLookup = tool({
name: 'cost_lookup',
description: 'Live AWS spend. Never estimate costs from memory. ' +
'groupBy SERVICE to find what spiked, LINKED_ACCOUNT to find where; ' +
'then join owners via edm_lookup. method',
inputSchema: z.object({
start: z.string(), end: z.string(), // YYYY-MM-DD, end EXCLUSIVE
granularity: z.enum(['DAILY', 'MONTHLY']),
groupBy: z.enum(['SERVICE', 'LINKED_ACCOUNT']).optional(),
service: z.string().optional(), // Example: "Amazon Bedrock"
}),
callback: async (q) => JSON.stringify({
results: shapeCost(await ce.send(buildQuery(q))),
source: { api: 'ce:GetCostAndUsage', params: q, queriedAt: new Date().toISOString() },
}),
})This function is unlike deterministic. It has an interface with a schema, with an API call JSON is returned. There are no algorithms with nearest neighbour, vectors, bayesian learning. In this way we can run unit tests on it. The agent is what loops around the model that decides when to call which tool, with what parameters, how many times and it now has the contract-bound answer with our two tools. This is essentially harness design, tools stay stupid and we can audit them. Agent performs the reasoning and the contract is the expert at keeping the history of minimum info of if it is validated or not and when. This means we also can keep track if a question has different answers the same day. Remember we wanted to measure correctness and accuracy.
Creating the agent
1
2
3
4
5
6
7
8
9
const agent = new Agent({
model: new BedrockModel({ region: 'eu-central-1' }),
systemPrompt: RULES, // "every claim (incl. costs) from a lookup; not_found is an answer; read-only"
tools: [edmLookup, costLookup],
structuredOutputSchema: GroundedAnswerSchema,
})
const result = await agent.invoke(question)
return GroundedAnswerSchema.parse(result.structuredOutput) With
structuredOutputSchema Strands validates the response against the contract and automatically retries with the validation errors as feedback. If it still doesn't validate, a StructuredOutputError is thrown, that is a harness catch instead of an assumption.
So to answer the question "Why did our Bedrock bill spike last month?":
The agent calls costLookup with variable monthly, groups it by service to check for the spike, also checks for daily Bedrock-filtered cost to dates, uses catalog to attribute it and owners for seeing department of spend.
The agent pulls monthly spend grouped by service to confirm the spike, checks daily Bedrock-filtered costs to date it, then switches to the catalog to attribute it, which services and owners that spend belongs to. Cost claims references
GetCostAndUsage, ownership references the EDM with a version. Great, now we have accuracy and audit trail.If someone in Slack asks the question of "Who owns quantum-db?", the code runs the lookup, returns nothing the contract fulfillment response is
not-found, that is "I don't know".1
2
3
4
{
"statement": "no service named quantum-db exists in the EDM", //agent response
"status": "not_found", "evidenceIds": []
}Now what would be in your Enterprise Data Model?
Mine has domain, teams, services and datastores. But what would your job role and colleagues care about? Tell me in the comments!
Find the complete code for this Proof of Concept in my GitHub: caroline-cah.
Thanks for reading, would love your feedback<3
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article