AI agents · Harness design

Inside the Skeleton of a Security Agent

Adapted from my course, Build Your Own Agentic Defense.

Watch on YouTube ↗

As we established in Agent = Model + Harness, the harness is the deterministic code that mediates the interaction between the model and our system. The most elementary action it must be able to perform is therefore communication with that model: sending a request and receiving a response. That exchange is the model call, and it is where we will begin our study of harness engineering for security. We will look at it at the API level so we can see exactly what our code sends and what the model returns.

At that level, the mechanism is much the same whether we are investigating an alert or writing a report. To give those calls a specific job, we organize them into a node, together with the deterministic code needed to carry out that job. Our code prepares the inputs, handles tool requests and any further calls, checks the result against the format we need, and decides where to store it. Designing different nodes allows us to create different types of agents within the same harness.

ONE HARNESS, THREE NODESONE NODE: TRIAGE AN ALERTrequestresultresponsepossibly. fixedintervals suggest anautomated process…INPUTSchosen by our codealert: same IP, ~45 srequesta workstation reachesone IP every ~45 s.suspicious?priority rulestask: set a priorityTOOLSpermitted by our codeget_assetfinance laptopOUTPUT CHECKthe format it needspriorityreasonP2beacon to a rare IPformat okstoretriage queueP2 · WS-0142model calltext in, text outcall 2inputstoolsoutput checkstoreTRIAGE AGENTalert + priority rulesget_assetpriority + reasontriage queuemodel callNODE 2inputstoolsoutput checkstoreINVESTIGATION AGENTcase + evidencequery_logsfindings + evidencecase recordmodel callNODE 3inputstoolsoutput checkstoreREPORTING AGENTfindings + audienceread_caserequired sectionsreport file
FOR EXAMPLE: REPORTING A reporting node could turn findings into a report for a chosen audience. The model call is the same in all three; the code around it makes each one a different type of agent.
§01

One Model Call

At its simplest, a model call is an exchange between our harness and the service running the model. What our harness sends to the service is the request; what the service sends back is the response. An API, or application programming interface, defines how our software makes that exchange, including the structure the request must follow and the form of the response it can expect.

The harness prepares the request before sending it. It assembles the input for the model, which can include instructions, the user's question, a transcript of earlier exchanges, results from tools, and additional context our code has retrieved, such as logs or documents. The request can also describe tools the model may ask to use. Alongside that input, it carries settings for the service, such as which model to run and a limit on the output. Choosing what to include is part of designing how our harness approaches the task.

For our first example, we will keep the request small: a general instruction, a question about network activity and a few settings. Here is a simplified request to Anthropic's API, with authentication omitted:

json
POST https://api.anthropic.com/v1/messages

{
  "model": "claude-opus-4-8",
  "max_tokens": 1024,
  "system": "Help a security analyst assess suspicious activity. Explain your reasoning and acknowledge uncertainty.",
  "messages": [
    { "role": "user",
      "content": "A workstation is reaching the same external IP every ~45 seconds. Suspicious?" }
  ]
}

The data we send in this example is written as JSON, a format for representing data in named fields that our software and the API can both read. The fields shown select the model, supply our instructions and question, and use max_tokens to limit the generated output. That limit is measured in tokens: pieces of text such as words or parts of words. The particular question can change from one call to the next while this basic structure stays the same.

The system prompt gives the model general instructions for approaching its work, while the user message supplies the particular question we want answered. Our harness brings both together in the request.

The service returns its response to our harness in a structured form too. In this example, it contains a generated answer to our question. Here is the part containing that answer:

json
{
	"role": "assistant",
	"content": [
		{
			"type": "text",
			"text": "Possibly. Fixed intervals like that suggest an automated process rather than a person…"
		}
	]
}

Our code can then read the answer and decide what to do with it. It might display the text to a user, pass it into another part of the application, or save it for later. This is what makes the API useful for harness engineering: the model's response becomes something our software can work with as part of a larger process.

§02

Nodes

We have now seen the request our harness sends and the response it receives. Let's zoom out to the node shown in Figure 1. Much of harness engineering lies in deciding what goes into those calls, which tools may run between them, and what happens to the result. These choices let us design different types of agents within the same harness.

Node

A defined job in the harness, together with the inputs, instructions, tools, limits and output requirements chosen for it.

Imagine giving every call all the evidence, every available tool and a broad instruction to handle whatever comes next. That flexibility can suit a general assistant. When we know what we need our security agent to do, we can be more deliberate.

CONTEXTwhatever seems usefulTOOLSany it reaches forwhatever text comes outREVIEW A FINDINGverdictconfidenceevidenceverdictsdatadatapromptprompttoolstoolsshapeshapestorestoreWRITE THE REPORTsummaryfindingsnext stepsreportsdatadatapromptprompttoolstoolsshapeshapestorestoremodel callany jobjob: review a findingjob: write the report"email these logs out"model callis this finding real?model callwrite it for a humanhost baselinesverdictsreport templateone findingrdap_lookupsearch_intelsearch_intelasset_owner
SAME ANATOMY, ITS OWN SET Every node has the same slots, and every choice in them differs. Sets can overlap where both jobs need it, as both may search threat intel, but each set is chosen for its job, not inherited.

Consider two nodes that might use the same model:

  • Assess a finding: Receive the finding and supporting evidence, use investigation tools, and return a judgment with reasons.
  • Write a report: Receive accepted assessments and a report template, use tools relevant to reporting, and produce an explanation a person can act on.

The model-call mechanism is the same. The inputs, instructions, tools and expected outputs give each node its purpose.

Clearer expectations

Defining each node separately lets us specify what a good result must contain and evaluate it against that expectation. We can test assessment quality and report quality independently, then adjust the instructions or output checks for the node that needs improvement.

Lower cost and more focused context

A report-writing node does not automatically need every log or tool used during an investigation. Leaving out irrelevant material reduces input-token cost and helps the model stay focused on the evidence and instructions that matter for its task.

A narrower security boundary

Each node gives us a defined scope for the permissions our harness must enforce. If a node can request every operation the harness offers, a mistake or injected instruction has more scope to cause harm. Access should follow from the node's role. Two nodes can share a tool when both need it, while the harness enforces the permissions chosen for each.

§03

One Node's Life

Building a node means making choices at three points: before the first call, between calls, and after an answer returns.

Before: prepare the inputs

Before the first call, we prepare the information and instructions the model will work from. We supply the material that initiated the task. We also load a skill containing our standardized procedure for that task: which checks to perform, when to seek more evidence, and what the result should contain.

The skill can explain when and how to use tools, while the harness supplies their definitions. Tools give the node access to capabilities its task requires, such as retrieving information, running a forensic analysis or taking action in another system. This also allows us to gather further context as needed, without loading it all into the first request.

Between calls: control the tools

When the model requests one of those tools, the harness decides whether that request is allowed. For example, a node that reviews security events might have access to an intelligence lookup, but no tools that change accounts or run commands on workstations. Describing those limits in a prompt is not enough; our code must enforce them.

We also check the arguments supplied with a request. A lookup that accepts a destination URL could restrict it to approved service domains. That destination is separate from the address being investigated: the tool can ask an intelligence service about an IP without connecting to that IP itself.

An approved tool runs between model calls. We include its result and the preceding exchange in the next request so the model can use what was found. This may lead to another tool request, so we also set limits on attempts, token use or spending. If the node reaches a limit before completing its task, it must report that incomplete outcome.

After: check and save the result

Once an answer returns, our software needs a dependable way to read it. For example, a node reviewing a security event might return a concern level and a reason. These fields form the canonical object: the agreed structure the rest of the application expects.

The harness checks that the required fields are present, the values have the right types and unexpected fields do not pass through. If the answer is malformed, it can request a correction within a retry limit. If no valid answer arrives, it reports the failure.

Valid structure does not guarantee a correct or safe answer. An attacker can place instructions in material the model reads, known as indirect prompt injection, and those instructions may reach an allowed text field. Downstream code must still treat that text as data and enforce its own permissions.

Finally, we decide where the accepted result goes. It might become input to another node, a database record or a file for later review. That choice follows from what the rest of the security workflow needs to do with it.

BEFOREBEFOREwhat it gets to knowDURINGDURINGwhat it may doAFTERAFTERwhat must survivesystem promptuser promptmessage historytelemetry and logssupporting contextwhich modelstrongestsmall, fast, cheapGATEallowed: ti.exampleLIMITStool attempts1 / 3tokens2,140 / 20,000dollars$0.01 / $0.25help.example: rejectedlookup_intel help.examplemodel calljob: rate this sign-inmodel: small, fastTEXT BACKSHAPE CHECKSTOREsign-in verdictsconcern: —why: —concern: highconcern: highwhy: new country, 03:12forward_to: help.exampleconcern: highwhy: new country, 03:12missing why: retriedpasses: known fields+1 rowTHE SIGN-IN NODE, AS CODEcontext assemblytool wiring and permissionslimit enforcementthe model callvalidation into the canonical objectthe write to the store
ALMOST NONE OF IT IS THE MODEL Step back, and the model call is a handful of lines: build a request, send it, take the response. Everything around it is ordinary code, and there is far more of it. The line counts are illustrations.
§04

Designing the Harness

With the model call, we saw how our code communicates with a model: a request goes in and a response comes out. We then zoomed out to the node, where inputs, skill instructions, tools and checks give those calls a defined purpose.

The exchange stays the same from one node to the next. What makes one node a triage agent and another a reporting agent is the code around it: the inputs we prepare, the tools we permit, the format we require and the place we keep the result. None of those choices belong to the model. They live in code we can read, test and change, and that is where most of the work of building a security agent happens.

We can zoom out again to the workflow: the arrangement of nodes and the paths work takes between them. The same nodes can form different workflows when we change how they connect or the order in which they run. Alongside designing each capability, we can now decide how those capabilities work together.

model callmodel callnodenodenode typesnode typesworkflowsworkflowsinputstoolsoutput checkstoretriagemodel calltext in, text outinvestigationreportinggategate
SAME NODES, ANOTHER WORKFLOW Change how the nodes connect and the same nodes form a different workflow: here a gate sends a low-priority alert straight to reporting. Nodes are components; the workflow is the next level up.

Connecting nodes raises new questions: what each node passes to the next, what a gate checks before work moves on, and what we keep once a run is finished. Those questions belong to the workflow, and we will take them up in later articles.

Keep reading

Go further
Available now

Ready to master agents for defensive security?

Start mastering agents for defensive security with my flagship, self-paced online course, designed specifically for defenders.

This lesson comes from Module 03, Agentic Foundation. From there, the course goes into Agent Data Interface, Tools, MCP, Skills, Initiation & Detection and more.

What’s included

  • 127 lessons
  • Hands-on labs
  • Build It Yourself guides
  • Lifetime course access
  • Course updates included
  • One-time purchase
References2
  • Messages API. Anthropic (Claude Platform docs). The request-and-response interface used in the example. platform.claude.com ↗
  • 12-Factor Agents. Dex Horthy / HumanLayer. Principles for designing the software around model calls, including prompts, context and control flow. github.com ↗