Repository navigation
FEAT: file write surface scorer (scorer phase 10) - #3042
WatchTree-19 wants to merge 6 commits into
Conversation
Adds SurfaceScorable and ScoringScope, the ContentWritten condition, a surface observation payload, LocalFileSurfaceSource and FileWriteScorer, with a runnable attack example.
Preserve both surface and conversation scoring additions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…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.
|
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 Bounded work: Confinement: each file is now opened first, the open handle's final path is checked against the resolved root ( Message reference: the scope now comes from |
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>
|
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. |
Description
This is phase 10 from the scorer roadmap, the surface prototype. It adds a scorer that answers "did this run write that content to that location?" by reading what the location actually holds, rather than what the response says it did.
It is built on the pieces from the earlier phases, so a surface is just another scorable with its own observation source, and nothing in the existing scorers changes.
What it adds
SurfaceScorable(scorable_type="surface") with auri, asurfacename, anexactorglobmatch, and an optionalScoringScope.ScoringScopewith a timewindow,labelsand anattempt_id, so a source can narrow the question to one run.ContentWrittencondition (uri,match, optionalcontains).SurfaceObservationPayload(kind="surface") holding oneSurfaceEntryper location (size, sha256, modified time, a bounded copy of the text) plusSurfaceCoverageand a count of locations that fell outside the scope.LocalFileSurfaceSource, which reads files under one root directory, such as a mounted sandbox workspace.FileWriteScorer, aTrueFalseScorerwithCONDITION_TYPE = ContentWritten.On the two open questions in the roadmap
ObservationSource[SurfaceScorable]. The local file source is the first one; a container, bucket or MCP filesystem source would plug into the same slot without touching the scorer.How the verdict works
Correlating a write to a run
Given a message, the scorer builds the scope from the run itself: the conversation's
attack_result_id(from phase 9) and a window from the first message, less a small clock skew allowance, to the time of scoring. The local source can only check the window, against file modification times, so it records in the observation metadata which scope keys it did not apply. A file planted before the run is counted as outside the scope and cannot make an attack succeed.Safety of the local source
Locations resolve inside the root only.
..is rejected, and symbolic links that point outside the root are not followed and are reported as a coverage gap. Reads are bounded bymax_filesandmax_content_bytes, and run off the event loop.Replay
The observation keeps the digest, size, modified time and retained text, so
score_observation_asynccan re-judge it against a newcontainsafter the workspace is gone. Replay against a different location raisesNonReplayableObservationError.Tests and Documentation
tests/unit/models/test_surface.py(17 tests): model validation, round trips through the scorable and condition registries, and the acquisition and coverage invariants onObservation.tests/unit/score/test_file_write_scorer.py(41 tests): the source (window, glob, limits, symlink escape, dangling links, truncation on a multi-byte boundary), the matching table, scorer verdicts, offline replay, and an end to endPromptSendingAttackagainst a local agent that writes the file, including a planted older file that must not count.doc/code/scoring/6_file_write_scorer(paired .py and .ipynb, executed), linked from the scoring overview,myst.ymlandframework.md. It needs no model, service or credentials.