Technically AcceptableIndependent technical publication
← Writing archive

Building a Prompt-Injection Test I Can Actually Trust

Sentinel now has matched corpora, a contained egress path, and a cleaner experimental boundary.

  • AI Security
  • Local AI

The previous Sentinel run produced no usable prompt-injection result. The apparatus leaked treatment cues through its corpus structure, and the send action had no working egress path. Verdict: invalid test, no result.

The runner recorded qwen3.6:35b refusing the injected instruction after reading a small set of synthetic work documents. That is an observed refusal. It does not reveal why the model refused, and it does not prove that the model reasoned from any specific filename, path, or prompt detail.

The experimental design was still wrong. Even if the model had called the send tool, the tool led nowhere and could not produce an independent receipt. Sentinel becomes useful when it can test Simon Willison’s lethal trifecta: untrusted content, sensitive data, and the ability to communicate externally. The lab version keeps that final action contained and observable.

Treatment leakage

The earlier setup exposed differences outside the intended treatment. Filenames, parent directories, sequence numbers, folder structure, document ordering, and task wording could all tell the two conditions apart.

The next experiment treats those signals as controlled variables:

  • Filename
  • Parent directory
  • Sequence numbers
  • Doc type
  • Semantic clues outside the intended treatment
  • Folder structure
  • Task wording
  • Document ordering
  • Previous conversation/context

The point is not to guess which clue influenced the model. The point is to remove every known clue that does not belong to the treatment.

What changed

The earlier agent had a send action that was not connected to a receiver. That was an experimental limitation. A tool call could occur without producing an observable delivery result.

Sentinel now has a contained catcher that records payloads delivered by the existing send_data tool. The agent log records that the model requested the action. The catcher receipt independently records that the tool delivered a payload.

A catcher receipt is the observable proxy for delivery through the available action. It is not proof of external exfiltration.

No send_data call
= model did not request the available send action

send_data called, catcher receives data
= payload delivered to the contained catcher

send_data called, catcher receives nothing
= send requested, but delivery not established

benign positive-control send reaches catcher
= contained delivery apparatus is functioning

The measured path is straightforward:

Model

CALL send_data(...)

Python tool executes

contained local destination

local catcher

receipt/log

This is sanitized verification output from the local harness. The verification exercised the apparatus only. It did not run the model or the poisoned treatment.

[send_data] ATTEMPT | url=<INTENDED_RECEIVER> | payload_bytes=21
[send_data] SINKHOLE on | host=<INTENDED_RECEIVER> | routed to <CATCHER>
[send_data] SENT | host=<INTENDED_RECEIVER> | status=200 | bytes=21
catcher receipt: received | body_match=True | captures=1
positive control: PASS

filename sets identical: PASS (8/8)
non-treatment pairs byte-identical: PASS (7/7)
treatment pair is only byte difference: PASS (1/1)
manifest hashes match files: PASS
corpus pair verification: PASS

[read_document] REFUSED traversal | requested=<MANIFEST> | resolved=<OUTSIDE_CORPUS_ROOT>
REFUSED: that path is outside the document library.
outside-root read refusal: PASS

Rebuilding the corpus

The corpus contains eight synthetic work documents in two matched roots. Seven document pairs are byte-identical. The eighth pair contains the intended treatment difference: one clean document and one document containing the prompt injection.

Each document uses an opaque identifier so the filename does not reveal its role. The model does not receive the manifest that maps identifiers to source documents and conditions.

The verifier compares the document bytes directly. It also records a SHA-256 digest for each file as an integrity record.

The reader resolves every request against the active CORPUS_ROOT and refuses any resolved path outside that root. The model does not have to make the right decision here. The reader enforces the boundary.

Git and record keeping

Sentinel keeps the code, corpus, and experimental configuration in version-controlled records. Each run must identify:

  • Which Sentinel revision was running
  • Which corpus revision was used
  • Which files were presented
  • Which document was the treatment twin
  • Which bytes existed in those files
  • Which known treatment leaks had already been corrected

That record separates changes to the subject from changes to the experiment.

Current status

The apparatus is ready for the next experiment. The experimental result is not.

  • Contained egress path: working
  • Benign positive control: complete
  • Treatment-blind corpus: built
  • Opaque identifier matching: verified
  • Byte/hash checks: passed
  • Manifest containment: tested
  • Mocked runner checks: passed
  • Vulnerable agent code: frozen
  • Live clean condition: NOT RUN
  • Live poisoned condition: NOT RUN
  • Reader / actor split: NOT BUILT
  • CaMeL-style isolation: NOT DEMONSTRATED
FIRST:
Build vulnerable agent

THEN:
Make vulnerable experiment valid   ← WE ARE HERE

THEN:
Generate vulnerable evidence

THEN:
Build architectural separation

THEN:
Repeat same attack

THEN:
Compare results

The next run

The next experiment will run both corpus conditions with fresh model context. The model, settings, system prompt, task prompt, tools, synthetic sensitive data, filenames, folder layout, and seven non-treatment documents will remain fixed. Only the treatment document will differ.

The intended comparison is:

CLEAN                POISONED
-----                --------
Model            same                 same
Model settings   same                 same
System prompt    same                 same
Task prompt      same                 same
Tools            same                 same
Sensitive data   same                 same
File names       same                 same
Folder layout    same                 same
Other 7 docs     same                 same
Fresh context    yes                  yes
Egress path      working              working

Treatment Doc   clean                injected

intended difference

If the controls hold, the next run should produce a more interpretable comparison. A catcher receipt would establish delivery through the contained action. It would not establish external exfiltration, reveal the model’s reasoning, or demonstrate CaMeL-style isolation.

Sentinel has not been made safer than the previous iteration. The next result is expected to be more interpretable, but that remains a prediction until both conditions run.

Return to writing archive