Skip to content

FEAT: observation lifecycle for surface scoring (scorer phase 11) - #3156

Merged
Richard Lundeen (richlundeen) merged 3 commits into
microsoft:mainfrom
WatchTree-19:feat/observation-lifecycle
Oct 14, 2026
Merged

Richard Lundeen (richlundeen) merged 3 commits into
microsoft:mainfrom
WatchTree-19:feat/observation-lifecycle

Conversation

@WatchTree-19

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

Copy link
Copy Markdown
Contributor

Description

Phase 11 of the scorer design plan, following the merged phase 10 (#3042). The contract is baseline-aware content-state comparison, not write-event detection or writer attribution.

ContentWritten.require_content_change=False preserves presence scoring, including with lifecycle-capable sources. Setting it to True requires final content to differ from its baseline and satisfy the matcher. A required comparison without a baseline is undetermined, not snapshot success.

  • SurfaceBaseline is retained in SurfaceObservationPayload for offline replay.
  • Optional SurfaceLifecycleSource and AttemptObserver protocols capture evidence before execution. Scorer-tree traversal preserves each child's routed expectation.
  • Startup failures record the allocated attempt's ERROR result. Cleanup attempts every observer and reports stop failures without replacing the outcome.
  • Content hashes and sizes establish state differences; modification times do not. Unrelated edits and appends can preserve existing matches and satisfy this predicate. Equal snapshots do not prove that no writes occurred: identical rewrites, transient writes, and restoration are not detected.
  • Attempts sharing a root still need exclusive access or serialization. The overlap guard within one scorer is not a lock or complete writer correlation. Actual write claims require separately recorded write-event evidence.

The plan and paired notebook now state this contract explicitly. The public scorer and condition names remain compatible with phase 10; the condition selects the predicate rather than the source's capabilities selecting it implicitly.

Tests and Documentation

Added attack, persistence, and replay coverage for unrelated edits, appends preserving a match, identical rewrites with changed or preserved timestamps, transient writes followed by restoration, missing baselines, and explicit presence versus change criteria. Existing wrapper, overlap, startup/cleanup, binary-content, and acquisition-bound tests remain covered.

Validation for 1af5bc34e:

  • uv run --no-sync pre-commit run --all-files — all hooks passed after notebook metadata normalization, including type checking.
  • 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 tests\unit\models\test_condition.py -q --tb=short — 259 passed, 3 skipped (Windows symbolic-link privileges unavailable).
  • uv run --no-sync jupytext --to ipynb --execute --set-kernel python3 doc\code\scoring\6_file_write_scorer.py — passed. Outputs retained; notebook and Python cell content match.

A snapshot shows that content is present; it cannot show that this attempt
wrote it. Lifecycle support is optional and lets a source record a baseline
when an attempt starts and finalize a later snapshot against it, so attempts
can share a workspace without losing write attribution.

- SurfaceBaseline records what the named locations held, with the same
  bounded evidence and coverage a snapshot keeps. A finalized
  SurfaceObservationPayload carries its baseline, so replay gives the same
  verdict.
- SurfaceLifecycleSource and AttemptObserver are runtime protocols.
  LocalFileSurfaceSource implements start_observing_async and
  finalize_async on the same bounded, confined read as acquire_async.
- An attack asks its objective and auxiliary scorers that observe attempts to
  start once the attempt ID is allocated, before anything is sent, and to
  stop when the attempt ends, whatever the outcome.
- FileWriteScorer records a baseline per attempt and routed condition, and
  scoring finds the attempt from the conversation's attack result. A location
  counts only when the attempt changed it and added the content: unchanged is
  a proven negative, the same bytes written again cannot be credited, an
  append is credited only for a match that reaches into the new text, and a
  location the baseline might have missed stays undetermined. Without
  lifecycle support the scorer keeps the snapshot.
Comment thread pyrit/score/true_false/file_write_scorer.py
Comment thread pyrit/executor/attack/core/attack_strategy.py Outdated
Comment thread pyrit/executor/attack/core/attack_strategy.py Outdated
Comment thread pyrit/executor/attack/core/attack_strategy.py Outdated
Comment thread pyrit/score/true_false/file_write_scorer.py Outdated
@romanlutz Roman Lutz (romanlutz) removed their assignment Oct 13, 2026
WatchTree-19 and others added 2 commits October 13, 2026 15:04
- Observation starts after setup, inside the strategy's execution, so a
  failure to start records the attempt's ERROR result under its allocated ID.
- Every observer is stopped even when one stop fails. A failed stop is logged
  and no longer replaces the attempt's error, cancellation or saved result.
- The attack walks wrapped scorers (composite, inverter, fallback and other
  wrappers), so a FileWriteScorer inside one records its baseline with the
  expectation its wrapper routes to it.
- A baseline cannot tell overlapping attempts apart. The docs keep the
  one-at-a-time requirement for a shared root, and FileWriteScorer leaves a
  change undetermined when another attempt observed the same location at the
  same time. SurfaceBaseline.concurrent carries this, so replay agrees.
- Without a text matcher, a changed nonempty location is credited from its
  digest, so binary files and files beyond max_content_bytes are no longer
  undetermined.

Each has a regression test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f6debee-86b3-43ff-a35f-40af5a3a2dd6
@richlundeen
Richard Lundeen (richlundeen) added this pull request to the merge queue Oct 14, 2026
Merged via the queue into microsoft:main with commit 873c339 Oct 14, 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.

3 participants