Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ All notable changes to miniVERL are recorded here. The format follows

## [Unreleased]

### Fixed

- A reward scaffold saved with a UTF-8 byte-order mark is no longer reported as
a syntax error. CPython strips the BOM when it reads a source file, so such a
scaffold imports normally; the static checker handed the leading `U+FEFF` to
`ast.parse`, which rejected it. The failure was fail-closed — a legitimate
file was refused, nothing unsafe was accepted — and a BOM still cannot hide a
top-level call. Found while verifying the published v0.6.3 wheel on Windows.

## [0.6.3] - 2026-08-05

### Security
Expand Down
10 changes: 8 additions & 2 deletions docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ after the exact release commit and its remote checks are green.

## v0.6.4 (in development)

- [ ] A reward scaffold saved with a UTF-8 byte-order mark parses instead of
- [x] A reward scaffold saved with a UTF-8 byte-order mark parses instead of
being reported as a syntax error. CPython strips the BOM when it reads a
source file; the static checker did not, so a scaffold written by a
Windows editor was refused as unparseable. Found while verifying the
published v0.6.3 wheel.
published v0.6.3 wheel. Two regressions cover it, including one proving a
BOM cannot hide a top-level call.
- [ ] `pinned-profile-smoke` remains excluded from the required-status-check
list because `verl-bridge.yml` filters on paths and therefore never
reports on an unrelated pull request. It still runs, and must pass, on any
pull request that touches bridge code. Revisit if the workflow ever loses
its path filter.

## v0.6.3 Security, artifact integrity and release-state hardening

Expand Down
4 changes: 4 additions & 0 deletions src/miniverl/bridge/reward_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,10 @@ def _finish(findings: list[dict[str, Any]]) -> dict[str, Any]:
except UnicodeDecodeError as exc:
check["detail"] = f"reward scaffold is not valid UTF-8: {exc.reason}"
return _finish([_finding("encoding_error", 0, "source is not decodable as UTF-8")])
# CPython strips a UTF-8 BOM when it reads a source file, so a scaffold
# saved by a Windows editor imports fine. Handing the leading U+FEFF to
# ast.parse instead reports a perfectly good file as a syntax error.
source = source.lstrip("")

try:
tree = ast.parse(source, filename=str(source_path))
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test_verl_bridge_reward_definition_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,35 @@ def test_findings_are_bounded(tmp_path: Path) -> None:
assert check["findings_truncated"] is True


def test_a_byte_order_mark_does_not_look_like_a_syntax_error(tmp_path: Path) -> None:
"""Found while verifying the published v0.6.3 wheel on Windows.

CPython strips a UTF-8 BOM when it reads a source file, so this scaffold
imports fine; handing the leading U+FEFF to ``ast.parse`` reported it as
unparseable instead.
"""
target = tmp_path / "reward_or_verifier_scaffold.py"
target.write_bytes(b"\xef\xbb\xbf" + VALID.encode("utf-8"))

check = inspect_reward_scaffold(target)

assert check["status"] == "ok"
assert check["verification_level"] == "interface_shape_verified"
assert check["findings"] == []


def test_a_byte_order_mark_does_not_hide_a_top_level_call(tmp_path: Path) -> None:
target = tmp_path / "reward_or_verifier_scaffold.py"
target.write_bytes(b"\xef\xbb\xbf" + (PRELUDE + "exploit()\n\n\n" + VALID).encode("utf-8"))

check = inspect_reward_scaffold(target)

assert check["status"] == "fail"
assert "top_level_call" in _categories(check)
assert check["code_executed"] is False
assert not (tmp_path / "PWNED.txt").exists()


def test_undecodable_source_fails_closed(tmp_path: Path) -> None:
target = tmp_path / "reward_or_verifier_scaffold.py"
target.write_bytes(b"\xff\xfe\x00invalid utf-8 \xc3\x28\n")
Expand Down