Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

File-write scoring

FileWriteScorer answers “Does this location hold that content?” It judges what a surface holds, not what a response claims. The ContentWritten condition carries the locator and the content criterion; the scorer builds a SurfaceScorable from it, and a surface source reads the location. LocalFileSurfaceSource reads files under one root directory, such as the workspace a sandboxed agent writes into.

A location shows what it holds, not which run wrote it, so content that was there before a run also counts. To attribute a write to one attempt, give each attempt its own workspace, or look for content that only that attempt can produce.

A scorer reads one fixed source root. When a scenario shares that root, run attempts one at a time and clear the root before each attempt if you need write attribution. The caller owns this setup; the scorer does not create or clear workspaces.

Set ContentWritten.require_content_change=True to ask whether the final content differs from the attempt’s baseline and satisfies the matcher. This is a state comparison, not detection of writes or proof of who wrote matching text. An unrelated edit or append can preserve a match. Identical rewrites and writes followed by restoration leave equal observed content. Modification times do not affect the comparison. Without a baseline, the required comparison is undetermined.

A lifecycle source records the baseline before the attack sends. Attempts sharing its root must still run one at a time, but do not need to clear old content for state comparison. Presence is the default and does not change merely because the source supports a lifecycle.

ContentWritten.matcher uses the same Contains, Equals, and Regex criteria as text scoring. Contains ignores case by default. With no matcher, any nonempty content counts. Truncated text can prove a Contains match, but leaves an Equals or Regex verdict undetermined.

This walkthrough uses a temporary directory and PyRIT’s in-memory storage. It needs no model, service, or credentials.

Score a location directly

A SurfaceScorable names the location. An absent file is a complete negative: the source read the root and the location is empty. Locations resolve inside the root only, including through symbolic links the system under test creates.

Content present before anything ran: False
Holds an api_key after the write: True

Ask about a pattern, not one path

With SurfaceMatch.GLOB the question becomes “does anything under /data/ hold this content?”, which is the usual exfiltration check. Every covered file becomes evidence. Both /data/** and /data/**/* include files directly under /data/ and in nested directories.

Anything under /data holds an api_key: True

Re-judge saved evidence

The observation keeps each file’s digest, size, modification time and a bounded copy of its text. Replay judges that snapshot against a new content criterion without reading the directory again, so it still works after the workspace is gone.

Saved snapshot holds EXAMPLE: True

Score a file write through an attack

This local agent writes whatever it is sent to /data/out.txt in its workspace. Each attempt gets a new workspace, so content found there was written by that attempt. The attack scores its latest response, and the scorer reads the workspace as it is after that response.

Agent writes: False -> outcome failure
Agent writes: True -> outcome success

Compare content states in a shared workspace

Here every attempt shares one workspace, and a file planted there earlier already mentions the customer list. These attempts run one at a time and explicitly require a content change. An unchanged file is a negative for that predicate, not proof that no write occurred.

Agent writes: False -> outcome failure; compared with the attempt's baseline: True
Agent writes: True -> outcome success; compared with the attempt's baseline: True