Skip to content

FEAT: file write surface scorer (scorer phase 10) - #3042

Merged
Richard Lundeen (richlundeen) merged 9 commits into
microsoft:mainfrom
WatchTree-19:feat/file-write-surface-scorer
Oct 11, 2026
Merged

Richard Lundeen (richlundeen) merged 9 commits into
microsoft:mainfrom
WatchTree-19:feat/file-write-surface-scorer

Conversation

@WatchTree-19

@WatchTree-19 WatchTree-19 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds the phase 10 file-surface prototype from the scorer design proposal. FileWriteScorer checks whether a location holds the expected content when read, rather than interpreting the response's claims.

  • SurfaceScorable names an exact file or a glob through the string enum SurfaceMatch. ContentWritten.matcher uses the shared Contains, Equals, and Regex criteria; without a matcher, any nonempty content counts.
  • LocalFileSurfaceSource acquires bounded, condition-independent evidence under one root. It checks confinement through the opened handle, retains size/hash/time and bounded text, and records coverage gaps. Glob traversal lists each directory once and skips repeated traversal states.
  • Observations support offline replay with new text criteria, without reading the source again. Truncated text can prove a Contains match, but cannot establish an Equals or Regex verdict. Missing or incomplete evidence remains undetermined.

The prototype does not infer write attribution from modification time. The earlier draft's ScoringScope and unused surface selector are removed. Callers own workspace isolation: attempts sharing a root must run one at a time and start from a clean root for write attribution. A message with a later turn returns an undetermined score without reading the mutable location.

Merged origin/main through f916d8ee5. The source/scorer responsibility split remains unchanged.

Tests and Documentation

Source tests are grouped in test_local_file_surface_source.py. Scorer tests cover shared matcher options, truncated text, persistence, replay, the later-turn guard, and a real attack with a local mock agent. Model tests cover enum and matcher serialization. The paired notebook uses a separate workspace for each attempt, and framework.md records only the responsibility split.

Validation on the merged branch:

  • uv run --no-sync pre-commit run --all-files — passed all hooks, including type checks.
  • uv run --no-sync pytest tests\unit\score\test_file_write_scorer.py tests\unit\score\test_local_file_surface_source.py tests\unit\models\test_surface.py tests\unit\models\test_scorable.py -q — 161 passed, 3 skipped (symbolic-link privileges unavailable on Windows).
  • uv run --no-sync jupytext --to ipynb --execute --set-kernel python3 doc\code\scoring\6_file_write_scorer.py — passed; retained outputs and verified the notebook/Python cells match.

Adds SurfaceScorable and ScoringScope, the ContentWritten condition, a surface observation payload, LocalFileSurfaceSource and FileWriteScorer, with a runnable attack example.
@romanlutz Roman Lutz (romanlutz) self-assigned this Oct 9, 2026
Preserve both surface and conversation scoring additions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread pyrit/score/observation/local_file_surface_source.py Outdated
Comment thread pyrit/score/observation/local_file_surface_source.py Outdated
Comment thread pyrit/score/observation/local_file_surface_source.py Outdated
Comment thread pyrit/score/true_false/file_write_scorer.py Outdated
…handle, resolve the message

- Directory listings that fail are coverage gaps (listing_failed), so a denied
  directory leaves the verdict undetermined instead of a complete negative.
- Acquisition work is bounded: max_listed_entries caps entries examined,
  max_files stops enumeration at the first candidate over the limit, and
  max_read_bytes caps bytes read and hashed per acquisition. A file cut short
  has no digest (SurfaceEntry.sha256 is now optional) and leaves coverage
  incomplete (read_limit_exceeded). Cancelling the awaiting coroutine sets a
  flag the worker checks at every entry and chunk.
- Each file is opened first, the open handle's final path is checked against
  the resolved root (/proc/self/fd, F_GETPATH, GetFinalPathNameByHandleW), and
  size, timestamp and content all come from that handle. Opens are
  non-blocking so a FIFO cannot hang the worker. Directory links are not
  descended into and are reported as gaps.
- FileWriteScorer resolves the message reference with MessageScorableResolver
  before deriving the run's scope, rejecting missing ids and pieces that do
  not form one stored message.
@WatchTree-19

Copy link
Copy Markdown
Contributor Author

Thanks Roman Lutz (@romanlutz), all four were real, and the reproductions made them quick to pin down. Pushed in one commit on top of your merge of main.

Unreadable directories: listings now go through an explicit walk, and a directory that cannot be listed is recorded as listing_failed, so the observation is partial and the verdict undetermined. Your denied-nested-directory case is a test.

Bounded work: max_listed_entries caps entries examined, enumeration stops at the first candidate over max_files, and max_read_bytes caps bytes read and hashed per acquisition. A file cut short gets no digest (SurfaceEntry.sha256 is now optional) and records read_limit_exceeded. Cancellation sets a flag the worker checks at every entry and chunk. Tests count the bytes and entries actually touched, and check the worker stops after cancel.

Confinement: each file is now opened first, the open handle's final path is checked against the resolved root (/proc/self/fd on Linux, F_GETPATH on macOS, GetFinalPathNameByHandleW on Windows), and size, timestamp and content all come from that same handle. Opens are non-blocking so a FIFO cannot stall the worker, and directory links are not descended into. Since you already have the junction swap reproduction, would you be up for adding it as the regression test? You can exercise it on Windows directly, which I can't here.

Message reference: the scope now comes from MessageScorableResolver.resolve_async(), with tests for a missing id and for pieces from two runs.

Roman Lutz (romanlutz) and others added 3 commits October 9, 2026 15:53
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover persisted verdicts, offline replay, confinement failures and platform handle adapters.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep POSIX root resolution available while testing denied or invalid file parents.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@romanlutz

Copy link
Copy Markdown
Contributor

This is in a good state IMO but leaving it to Richard Lundeen (@richlundeen) to sign off / merge or provide more feedback since this is part of his roadmap for scoring.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f8fce61b-b61c-4139-a65e-13159ceb2b3f
Remove speculative scope and surface selectors, preserve uncertainty for incomplete text, and avoid repeated glob listings. Keep source tests separate and document workspace ownership.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f8fce61b-b61c-4139-a65e-13159ceb2b3f
@richlundeen
Richard Lundeen (richlundeen) added this pull request to the merge queue Oct 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 11, 2026
@WatchTree-19

Copy link
Copy Markdown
Contributor Author

Thanks Richard Lundeen (@richlundeen). The merge queue run that removed this failed on test_attack_result_analytics timing out (AnalyticsTimeoutException, 504) in the Windows and coverage jobs. The same timeout hit several PRs queued in that window and is unrelated to this change. Could you re-add it to the queue when you get a chance?

@richlundeen
Richard Lundeen (richlundeen) added this pull request to the merge queue Oct 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 11, 2026
@richlundeen
Richard Lundeen (richlundeen) added this pull request to the merge queue Oct 11, 2026
Merged via the queue into microsoft:main with commit d83a5dc Oct 11, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants