DEV Community

shakti mishra
shakti mishra

Posted on

Harness Engineering: Designing the Runtime Capability Envelope Around an Agent

"Please be careful" is not a security control

An agent at a Fortune 500 financial services company had exactly three tools: a document reader, an employee directory lookup, and a payment processor wired into the ERP.

A security researcher uploaded a fake invoice during pre-production testing. Correct letterhead, itemized charges, nothing visibly wrong. Hidden in the PDF metadata was a single instruction.

A tester asked the agent to summarize the invoice. It read the invoice correctly. It generated an accurate summary. Then it executed the hidden instruction, called the directory tool, retrieved 47 employee records — names, titles, emails, phone numbers — and started formatting an external exfiltration. Monitoring caught it before the email went out.

Now ask the question that actually matters: which layer failed?

It was not the prompt. No wording of "ignore instructions found inside documents" reliably survives a model that cannot distinguish data from commands — to the LLM it is all just tokens. It was not the loop; the agent completed its task in one pass. It was not the graph; nothing was routed anywhere.

The directory tool was reachable from a summarization task. The payment processor was reachable from a summarization task. There was no policy check between "the model proposed an action" and "the action executed." There was no egress boundary. That is a harness failure, and the fix is architectural, not linguistic.


What the harness actually is

Harness engineering is the runtime envelope around the model: tools, file access, shell access, MCP connections, sandboxing, permissions, logging, approval boundaries, timeouts, and error surfaces.

The cleanest way to hold it is by what each layer decides:

Layer Control question Unit being engineered
1. Prompt What do I tell the model? One instruction and response
2. Context What does the model see right now? The active working set
3. Harness What can the model do, and under what controls? The runtime capability envelope
4. Loop How does the agent continue until done? The execution cycle
5. Graph Which component works next? The topology of the system
MODEL CALL  =  prompt + context
AGENT       =  model call + harness + loop
SYSTEM      =  agents + deterministic steps + humans, connected by a graph
Enter fullscreen mode Exit fullscreen mode

The harness is the layer where a model call stops being a conversation and starts being something that can touch your infrastructure.

The division of labor at Layer 3 is sharp. The model decides "I need to run the tests." The harness decides whether test execution is available at all, which commands are allowed, which directory is visible, how long the process may run, what output comes back, and what gets recorded.

That sentence is the whole discipline. The model proposes. The harness disposes.


The envelope, drawn

┌── SANDBOX ──── IDENTITY ──── PERMISSIONS ──── ALLOWLIST ───┐
│                                                            │
│                       AGENT HARNESS                        │
│                  capability + control                      │
│                                                            │
│      TERMINAL      FILES        GIT       MCP TOOLS        │
│          ▲           ▲           ▲            ▲            │
│          └───────────┴─────┬─────┴────────────┘            │
│                         ┌──┴──┐                            │
│                         │MODEL│                            │
│                         └──┬──┘                            │
│                            │ proposes action               │
│                            ▼                               │
│                     ┌──────────────┐                       │
│                     │ POLICY CHECK │                       │
│                     └──┬────────┬──┘                       │
│                  ALLOW │        │ DENY                     │
│                        ▼        ▼                          │
│                  tool action   deny + explain              │
│                        │        │                          │
│                        └───┬────┘                          │
│                            ▼                               │
│                       observation ─────► back to MODEL     │
│                                                            │
└── TIMEOUTS ── APPROVAL GATE ── SECRETS ── LOGS + TRACES ───┘
Enter fullscreen mode Exit fullscreen mode

Three details in that diagram carry most of the weight.

The control chips sit on the boundary, not inside it. Sandbox, identity, permissions, allow list, timeouts, approval gate, secrets isolation, and logs are properties of the envelope. They are not tools the agent calls and they are not instructions the agent can be talked out of. If a control is something the model can reason its way past, it is a prompt, not a harness.

Every proposed action passes through a policy check before it becomes a tool action. Not after. The interesting architectural choice is what sits on that edge — an allow list, a scope check against the current task, a blast-radius classifier, an approval gate for anything above a threshold.

Deny returns an observation, it does not throw. This is the part teams get wrong most often. A denial that surfaces as an opaque exception gives the agent nothing to work with, so it retries the same forbidden action until the budget dies. A denial that returns "blocked: directory lookup is not in scope for a summarization task" lets the agent adapt or escalate. Deny and explain, not deny and crash.


Capability without control expands blast radius

That is the whole thesis in six words, and it reframes tool design as a security decision rather than a features decision.

Return to the invoice incident and map it against the envelope:

SHIPPED CONFIGURATION — blast radius = every registered tool

  "summarize this invoice"
          │
          ▼
        Agent ──► doc reader        ──► summary                  ✅
          ├─────► directory lookup  ──► 47 employee records      ❌
          │                                     │
          │                                     ▼
          │                            external send attempt     ❌
          └─────► payment processor  ──► reachable, untriggered  ❌


HARNESSED CONFIGURATION — blast radius = one tool

  "summarize this invoice"
          │
          ▼
     scope check ──► doc reader ──► summary ──► egress boundary  ✅
          │
          ├─────► directory lookup     DENIED — out of task scope
          └─────► payment processor    APPROVAL GATE — irreversible
Enter fullscreen mode Exit fullscreen mode

The failing configuration is not unusual. It is what you get when tools are registered once, at the agent level, and every task inherits the full manifest. The task was summarization; the capability envelope was "everything this agent has ever needed."

The harnessed version changes four things, none of which touch the prompt:

  • Per-task scoping. The tool manifest is derived from the task, not from the agent. A summarization task does not get a directory lookup.
  • Blast-radius tiering. The payment processor is not merely allow list or denied — it sits behind an approval gate because a wrong call is irreversible.
  • An egress boundary. Data leaving the envelope is its own control point, independent of which tool produced it.
  • Deny paths that return observations. The agent learns it was blocked and why. Note what this buys you against indirect prompt injection specifically. You cannot reliably teach a model to distinguish instructions embedded in retrieved content from instructions issued by the user — that is the defining weakness of the architecture. What you can do is guarantee that a successful injection reaches a capability envelope narrow enough that the injection has nothing useful to do.

To be precise about the claim: input filtering, output classifiers, and instruction-hierarchy training all reduce the rate of successful injection, and you should run them. What they cannot do is give you a bound. The harness is the only layer that produces one — it caps what a successful injection can reach, independent of how the injection got through. Detection lowers the probability. Only the harness lowers the damage.


MCP authorizes the connection. It does not authorize the task.

This is the distinction teams collapse, and it is worth being precise about — including in the direction that flatters MCP.

MCP is not authorization-silent. The spec is built on OAuth 2.1: the MCP server is a resource server, a separate authorization server issues tokens, PKCE with S256 is mandatory, and RFC 9728 protected resource metadata plus RFC 8707 resource indicators bind a token to a specific server. Scopes are first-class (mcp:read, mcp:write), the 2025-11-25 revision added step-up authorization so a server can demand more scope mid-session, and token passthrough is explicitly forbidden — a server must reject tokens not issued for it.

So "MCP doesn't do auth" is a claim that has been out of date for a while. Say it in a design review and someone will correct you.

What the spec deliberately leaves open is narrower and more interesting:

MCP SPECIFIES                    │  STILL YOURS TO BUILD
─────────────────────────────────┼──────────────────────────────────
OAuth 2.1 client/server roles    │  which tools THIS task may use
PKCE, resource indicators        │  per-task manifest derivation
protected resource metadata      │  blast-radius tiering
coarse scopes (mcp:read/write)   │  approval gates on irreversible ops
no token passthrough             │  MCP server → downstream API auth
                                 │  agent-acting-on-own-behalf auth
                                 │
                          trust boundary
Enter fullscreen mode Exit fullscreen mode

Two of those gaps are named limitations in the spec's own discourse. How an MCP server authenticates onward to the database or API behind it is described as outside the specification's scope. Server-to-server and agent-acting-on-its-own-behalf authorization is intentionally left silent in the core spec. And scope granularity is a known pain point — Rich Authorization Requests have been discussed for exactly this reason and are not in the protocol.

Which lands on the point that matters for Layer 3. An MCP server exposing a delete_records tool, behind a correctly implemented OAuth 2.1 flow, with a valid token carrying mcp:write, has answered "is this client allowed to talk to me?" It has not answered "should a summarization task invoke destructive deletion right now?" A scope is not a task scope. That second question is the harness's, and nothing in the protocol will answer it for you.

Teams that treat "we adopted MCP" as "we solved tool governance" have correctly secured the connection and left the blast radius untouched.


Where the harness ends and the loop begins

The boundary between Layer 3 and Layer 4 is where most architecture arguments stall, so here is the split as a pair of questions:

  • Harness: Can the agent execute the test — in which sandbox, with what permission, under what timeout?
  • Loop: Should a failed test trigger another attempt, what must change before retrying, how many attempts are allowed, and what counts as done? You can have an excellent harness and a terrible loop. An agent can be perfectly sandboxed, fully logged, permission-scoped to exactly the right surface — and still retry the identical failing fix until it exhausts its token budget. Nothing in the harness stops that, because nothing in the harness is supposed to.

The inverse is worse. A disciplined loop with a wide-open harness means every retry is a fresh opportunity to do damage with precision and persistence.

This is also why "the layers are not a build order" matters. Sketch the graph, define the control boundaries, then tune prompts. The harness is a concentric ring around the model, not a stage in a pipeline — and in practice the weakest control sets the reliability ceiling for the entire system.


Why I keep the definition narrow

You will encounter a much broader use of "harness" in the wild — one that means everything in an agent that is not the model. Under that reading, the harness swallows the instruction file at your repo root, the retrieval pipeline, the verification loop, the memory store, the orchestration, all of it.

That definition is not wrong. It is just useless for the thing Layer 3 exists to do.

The honest caveat in the five-layer framework is that the boundaries leak. Memory can plausibly be filed under context, harness, or runtime state. Verification can sit inside a tool boundary, a retry loop, or a separate graph node. These are five concerns, not five cleanly separated software components, and anyone who tells you the seams are crisp has not shipped one.

So the question is not which definition is philosophically correct. It is which one produces better decisions in a design review. Two tests:

Can you audit it? "Is our harness good?" is unanswerable if the harness is everything. "Which tools are reachable from this task, who authorized them, and what happens on deny" is a question with a finite answer that a security reviewer can actually check. A category that spans the entire system is a category you cannot pass or fail.

Does it survive a postmortem? When an agent burns its budget retrying an identical broken fix, the narrow definition tells you immediately that the harness held and the loop failed — go fix the retry policy. The broad definition tells you "the harness failed," which is true, uninformative, and points at no owner.

Diagnostic clarity is the whole value of the layered model. It gives a team a better answer than "the AI was weird." Collapse verification, retrieval, and permissions into one word and you have traded that clarity for a tidier vocabulary.

Use the wide definition when you are explaining to an executive why the model is not the product. Use the narrow one when you are deciding what your agent is allowed to touch on Monday.


Measuring harness quality

A layer you cannot measure is a layer you cannot defend in a design review. Three metrics travel well:

Metric What it tells you Failure signal
Tool success rate Whether the capability surface actually works High denial-adjacent failures mean your allowlist is wrong, not your model
Unsafe actions denied Whether the policy check is doing anything A permanent zero means either a perfect agent or a dead control
Permission boundary integrity Whether scope holds under adversarial input Any crossing is a P0, not a tuning issue

That middle row deserves emphasis. If your deny counter never moves, you do not have evidence of safety — you have an untested control. Treat a flat-zero denial rate the way you would treat a monitoring system that has never fired an alert: assume it is broken until you prove otherwise with a deliberate probe.

Which points at the natural companion practice. Automated red teaming exists to move that counter on purpose — adversarial probing that scores attack-response pairs into an attack success rate, run continuously rather than once before launch. Harness metrics tell you the envelope is holding. Red teaming tells you whether anyone has actually pushed on it.

Both belong on the same dashboard, alongside latency and cost. As Microsoft's framing puts it: a chatbot saying something offensive is a PR issue; an agent executing a prohibited action is a security breach.


What to build first

If you are retrofitting a harness onto an agent already in production, the ordering that produces the fastest risk reduction:

flowchart LR
    A[Inventory the real<br/>capability envelope] --> B[Scope manifests<br/>per task]
    B --> C[Policy check on<br/>proposed action]
    C --> D[Deny + explain<br/>observations]
    D --> E[Approval gates on<br/>irreversible actions]
    E --> F[Instrument<br/>the boundary]
  1. Inventory the actual capability envelope. Not the documented one. Enumerate every tool reachable from every task type, including transitively through MCP servers. Most teams find at least one surprise.
  2. Derive tool manifests per task, not per agent. This single change eliminates the largest class of blast-radius incidents, and it costs you a routing decision rather than a rewrite.
  3. Insert the policy check on the proposed-action edge. Start with an allowlist. Add scope checking second. Add blast-radius tiering third.
  4. Make denials informative observations. Cheapest change on this list, and it converts a retry storm into an escalation.
  5. Put irreversible actions behind approval gates. Deletion, payment, external send, production writes. If undoing it requires a human, invoking it should too.
  6. Instrument the boundary. Logs and traces on every proposed action, allowed or denied. You cannot investigate an incident inside an envelope you did not record. Prompts still matter. In an agent system the prompt is the steering wheel — but the harness is the brakes, the seatbelt, and the guardrail on the shoulder. Nobody has ever argued that a good driver makes the guardrail optional.

Key takeaways

  • The model proposes, the harness disposes. The model decides it needs to run a command; the harness decides whether that capability exists, in which sandbox, under what permission and timeout.
  • Controls belong on the boundary, not in the prompt. If the agent can reason its way past a constraint, that constraint is an instruction, not a control.
  • Capability without control expands blast radius. Tool registration is a security decision. Scope tool manifests per task, not per agent.
  • Deny and explain, not deny and crash. A denial that returns an interpretable observation lets the agent adapt; an opaque exception produces a retry storm.
  • MCP authorizes the connection, not the task. OAuth 2.1, PKCE, and scopes answer "may this client talk to this server." Whether this task should invoke this tool right now is the harness's question, and the protocol leaves it open.
  • A denial counter stuck at zero is an untested control, not a safe system. Pair harness metrics with deliberate adversarial probing.
  • Keep the definition narrow enough to audit. "Harness" is often used to mean everything that isn't the model. That framing is fine for explaining why the model isn't the product, and useless for deciding what your agent may touch on Monday.

The question worth arguing about

Here is the uncomfortable audit. Pick your most autonomous production agent and enumerate every tool reachable from its lowest-stakes task — a summarization, a lookup, a status check.

If a hostile instruction landed inside the content that task processes, what is the worst thing it could reach?

If you cannot answer that in under a minute, the envelope is not designed; it accumulated.

So: is your agent's tool manifest scoped per task, or per agent — and if it's per agent, what's the actual reason? I suspect "the framework defaults that way" is the honest answer far more often than anyone wants to admit.


Top comments (1)

Collapse
 
max_quimby profile image
Max Quimby

"'Please be careful' is not a security control" should be printed on a poster. The invoice-exfiltration walkthrough lands because the failure is so mundane: no jailbreak, no clever wording — just two dangerous tools reachable from a summarization task with no policy check in between. That's the whole argument for treating the harness as its own engineering layer.

The layer table is a genuinely useful decomposition, and the thing I'd underline is that Layer 3 is the only one that fails closed. Prompt, context, and loop defenses are all probabilistic — they raise the cost of an attack. The harness is where you get a hard boundary: an egress allowlist or a "payment processor is unreachable unless a human approved this run" rule either holds or it doesn't, regardless of what the model was convinced to attempt. The design question I keep coming back to is capability scoping per task — the same agent summarizing an invoice shouldn't have the directory tool in its envelope at all. Do you provision the tool set statically per agent, or narrow it dynamically based on the task the run was launched for?