Skip to content

Bundle files are not byte-stable across platforms: text-mode writes plus autocrlf break recorded digests #36

Open

Description

@how2how2how2-arch

Problem

Bundle files are written in text mode without newline="", so on Windows the writers turn \n into \r\n. Bundle digests are SHA-256 over file bytes, so the same trace then produces different bytes and a different digest per platform.

This came out of adding the cross-platform smoke job (#35): the job passes on Windows, but it does not read any committed bundle, and while checking that I could see the format itself is not byte-stable across platforms.

Where

Writers in src/stepfork/trace/storage.py, none of which opt out of newline translation:

  • _write_manifest — path.write_text(...)
  • _write_events — path.open("w", ...) for events.jsonl
  • _write_redactions — path.write_text(...)

Readers of those bytes:

  • trace/integrity.py::_hash_file — path.open("rb"), SHA-256 over the bytes
  • export/receipt.py::bundle_digest / file_sha256 — same, per file in sorted order

Two consequences

1. A bundle written on Windows does not reproduce a digest recorded elsewhere. Measured here by rewriting a committed bundle's files as CRLF in a temp copy, which is what a Windows write or a Windows checkout produces:

bundle: legacy_v01.sftrace
files rewritten to CRLF: 3
digest before: 4cdc805dda5e69e84d8327355d16d9eb8b631b15a3c43d078897511eabb7ec5a
digest after : 2d8f91b50e6266e95de9d6c72103aa6d56bfa5c2db66eb3e82fbb080ec3c9dea
DIGEST CHANGED: True

2. A Windows checkout corrupts the committed bundles before any test runs. There is no .gitattributes in the repo, so git's default core.autocrlf=true on Windows converts them at checkout. That is user-visible through stepfork validate --verify-integrity, which is opt-in and therefore unaffected on Linux CI:

# as committed (LF)
$ stepfork validate --verify-integrity langgraph_failure.sftrace
Validation passed.

# after rewriting the same bundle as CRLF, i.e. a Windows checkout
$ stepfork validate --verify-integrity langgraph_failure.sftrace
✗ integrity_mismatch
  SHA-256 mismatch.
  redactions.json
Validation failed: 3 issues.        (exit 1)

Scope

  • 35 committed files live under a .sftrace directory (26 .json, 9 .jsonl), across tests/golden (27), examples/real-llm-case-study (4) and examples/langgraph_agent/regression (4).
  • 8 of those bundles carry an integrity.json, so they are the ones --verify-integrity can reject today: tests/golden/{diff_baseline,diff_changed,failed_llm_run,failed_tool_run,partial_run,successful_run}, examples/langgraph_agent/regression/langgraph_failure, examples/real-llm-case-study/traces/incident-triage. All eight validate on LF on this machine.
  • events.jsonl is JSONL, so a CRLF file is still parseable line by line — the breakage is in the digests, not in parsing. Worth stating because "it still loads" is not evidence of no problem.

Fix

Two parts, both in the linked PR:

  1. newline="" on all three writers, so the bytes written are LF on every platform.
  2. A .gitattributes rule so a checkout cannot rewrite the committed bundles: **/*.sftrace/** text eol=lf.

On the pattern: *.sftrace/* and *.sftrace/**/* do not match nested files (checked with git check-attr), **/*.sftrace/** does. Narrow on purpose — other files in the repo keep git's default behaviour, and none of the 35 bundle files changes in the working tree as a result, so there is no renormalisation commit hiding in this.

Test note

Two of the three new tests can pass on POSIX with or without the fix, and I would rather say that than imply coverage I do not have:

  • "saved bundle files contain no CRLF" is the assertion that fails on Windows if newline="" is dropped; on POSIX the translation is a no-op, so it is a guard for a platform CI can reach but this machine cannot.
  • "bundle digest changes when a file gains CRLF" describes the stake, not the fix, and passes either way.
  • The .gitattributes test does fail on every platform when the rule is removed — verified by deleting the file and watching it go red.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions