NESSCOAgentic Harness
Architecture

How the harness works.

What sits where, what every tool call costs, what we use models for and what we deliberately don't, what this stops and what it doesn't.

1 · Two pieces

Deciding and enforcing are separate processes. The enforcement point sits on the tool-call boundary inside your workload and does one thing: block, hold, redact or allow. The decision point evaluates a signed policy bundle and returns a verdict. It never touches your traffic.

Splitting them buys three things worth having. Decisions are replayable — feed the logged call and the bundle back in and you get the same verdict, which is what makes an incident review possible. The thing on the hot path stays small. And policy ships as a signed file with a checksum, so "which rules were live at 04:12" has an answer instead of a recollection.

What happens if the decision point is unreachable

Your call. The bundle declares it per tool class. Deny is right for anything with a large blast radius and wrong for read-only retrieval, where it turns a control-plane blip into an outage. Anyone who tells you there's one correct answer hasn't run this in production.

2 · What happens on every tool call

Synchronous, before the call executes — the only ordering that can actually prevent anything.

capture hook the call, freeze the arguments normalise decode and canonicalise inspect run the detectors decide evaluate the policy bundle enforce allow · redact · hold · block log append to the audit chain (off the hot path) Normalising before inspecting is the part people get wrong. Detectors that read raw arguments are beaten by base64, URL-encoding or a homoglyph. Canonicalise first.

What it costs

Target is under 10ms added at p99. That number only means something with the budget behind it:

Stagep99Why
Capture + normalise< 0.5 msString work. No I/O.
Context lookup< 1 msLocal cache. A miss is the tail.
Pattern detectors< 2 msCompiled patterns over a size-capped payload.
Classifier< 4 msSmall model, quantised, kept in memory. The dominant cost.
Policy evaluation< 1 msMatching against a compiled bundle.
Audit write0 ms inlineBuffered, with back-pressure.

Two things follow. Payloads are size-capped before inspection, because detector cost scales with length and an unbounded input is a way to DoS your own control path. And a large language model cannot sit here — a call to one is hundreds of times over this budget. That's why judging happens on the hold path, which is asynchronous.

3 · Where we use models, and where we don't

"We use AI" isn't an architecture. The rule is: use the cheapest thing that's accurate enough, and only reach for a model when the cheap thing measurably isn't. Two of these three layers aren't machine learning.

Patterns — no model

API keys, tokens, private keys, destination allow-lists. These have fixed formats. An AWS key is AKIA plus twenty characters; a PEM block has delimiters. A regex and an entropy check catch these essentially every time, in microseconds, and tell you exactly what matched and where.

Putting a model here would be worse: slower, it would miss things the pattern catches outright, and it replaces "matched a credential pattern at offset 412" with a number between 0 and 1. If someone runs a classifier over your payload to find an API key, they're selling you the model, not the result.

Classifier — this is what the model is for

Prompt injection, jailbreak attempts, retrieved content that's quietly instructing the agent. There's no fixed format here. The attacker writes English, and "ignore previous instructions" and "disregard the earlier system message and instead" share almost no words and one meaning. A pattern list for this grows forever and is beaten by rephrasing.

A small fine-tuned model handles it, runs quantised and in-memory, and fits the 4ms budget. That's the whole justification, and it's testable: the input is adversarial English, and no list of rules covers it.

Judge — never inline

When the classifier lands in the uncertain band, the call is held and a bigger model writes up what it thinks for whoever reviews it. Asynchronous, always. It's too slow for the hot path, and it samples, so it isn't replayable.

How to hold us to it

A classifier on an enforcement path is a liability unless you know its error rate. The threshold is set from how often you're willing to be interrupted, not from a headline accuracy figure — accuracy is close to meaningless here, since "everything is fine" scores well when almost everything is.

interruptions per hour = false-positive rate × calls per hour Published each release, on attacks it hasn't seen: · catch rate per attack type, at the threshold we actually ship · false-positive rate on normal traffic · latency p50 / p99 under real load Every model version has a digest, and that digest goes into the audit record of every call it touched. Months later you can still answer "which model made this decision".

What we don't claim

No classifier catches every injection. Attackers adapt, and any detector that ships becomes something to train against. That's why there are layers: blast-radius caps, human holds and the kill switch are what limit the damage when the classifier is beaten — and it will be. A detector sold as the whole answer is a single point of failure.

4 · What this stops

The agent is treated as untrusted — not because it's malicious, but because what it does is a function of input an attacker can write.

AttackWhat it looks likeWhat stops it
Indirect prompt injectionInstructions hidden in a fetched page, a document, tool output or an imageClassifier on ingest; content from untrusted sources can't widen what the agent may call
Confused deputyA low-privilege agent reaching a high-privilege tool through a helperCapabilities scoped per agent, checked through the chain rather than per call
Slow exfiltrationRecords leaked a few at a time through a destination that's on the allow-listOutbound content checks plus volume and rate budgets per destination
Tool-chain escalationA read-only agent reaching a shell via calls that are each individually fineRules that match the chain, not the single call
Supply chainA tampered or typo-squatted tool definitionSigned tool manifests; unknown tools denied by default
Audit tamperingEditing history to hide what happenedHash-chained records — the edit shows up at verification

What it doesn't stop: a compromised host, an insider who can sign policy bundles, or an attacker holding both your database and your signing key. Those need controls underneath this one. A security product that won't tell you where it stops isn't describing a threat model.

5 · The audit log

One record per decision — the call, what the detectors found, the verdict, which policy and which model version. Each record includes a hash of the one before it.

That proves nothing has been edited or quietly removed since the next record was written; verification walks the chain and tells you the first index where it breaks. It doesn't prove anything against someone who holds the log and can rewrite every record after the one they changed. Publishing the latest hash somewhere you don't control fixes that; without it you have integrity evidence, not proof.

Findings record the type and position of what was found, never the matched text. A log that quotes the secret it detected has just copied that secret somewhere with weaker access controls and longer retention.

6 · Where it runs

ModeWhere enforcement sitsTrade-off
In-processLibrary, on the tool-call boundaryFastest, sees the most. Shares the agent's process.
SidecarLoopback proxy next to the workloadIsolated from the agent, costs a local hop. The usual choice.
GatewayProxy in front of the tool endpointsOne place to upgrade. Sees the request, not the reasoning.
Air-gappedSidecar plus local decision point and modelsNo outbound dependency. Updates arrive as signed files.

The further enforcement sits from the agent, the less it can see. At the gateway you get an HTTP request. In-process you get the tool the agent chose, the arguments it built and the task it thought it was doing — which is the real difference between this and a WAF.

7 · Attacking your own setup

A control you've never attacked is a guess. The same path runs in attack mode against your agents, in your environment, and the suites run in CI as regression gates — lose catch rate on an attack type, or gain false positives past budget, and the build fails.

SuiteWhat it tries
InjectionDirect, indirect and multi-turn, through retrieval, tool output and images
ExfiltrationEncoding, chunking, timing, abuse of allow-listed destinations
EscalationMulti-hop chains toward something privileged
Over-blockingBenign requests near the line, to keep false positives honest
EvasionRephrasing, homoglyphs, base64, zero-width characters, language switching

Every finding comes with the exact call, the policy version and the model versions, so a fix is something you can verify rather than take on trust.

Scope

This is for testing systems you own or are contracted to test. Engagements are scoped in writing and run against your infrastructure. The corpus ships as detection fixtures for hardening, not as payloads to point at anyone else.