Skip to content

Read worktree paths as bytes; say which commits a release is - #13

Merged
Deep-Sixed merged 2 commits into
mainfrom
claude/relaxed-brown-dzjej0
Sep 28, 2026
Merged

Deep-Sixed merged 2 commits into
mainfrom
claude/relaxed-brown-dzjej0

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Work package: the two findings from the v0.3.1 review. Branched from main at 3120337. It keeps contract evidence-v3.2, and the version bump to 0.3.2 follows in its own PR, as #10 did.

commit finding fix
cf878d8 P2. Paths and symlink targets were decoded as strict UTF-8. One untracked file named bad\xffname ended run, baseline and attest in a UnicodeDecodeError: exit 1, which means FAIL here, not BLOCKED. os.fsdecode for git's path bytes and os.fsencode wherever a path or link target goes into the digest. A path that can't be read is now BLOCKED (exit 2), naming the path.
2e78da6 P2. INSTALL.md said "There are no release tags yet" and that a tag would become the thing to pin. Tags exist and can still be moved. The pin section says to pin a full commit id. A table names each release by its real commits: v0.3.1 → 84418d8, and the runtime it pins, 819b7c2. No tag policy change.

What the tests decided

  • No state ID changes. For valid UTF-8 paths, fsdecode/fsencode produce exactly the old bytes. test_valid_utf8_state_ids_are_unchanged_from_the_strict_verifier loads the v0.3.1 verifier and compares state_id and state_paths over a tree with non-ASCII names, a staged rename, symlinks and a nested repository. They are identical.
  • drift_paths needs no new encoding. A gate that creates made\xff produces drift that is BLOCKED and named in the record, under both evidence/2 and evidence/3. The record is ASCII ("made\udcff", via json.dumps' default escaping) and loads back unchanged. test_a_record_naming_an_undecodable_path_survives_emit_and_load pins that emit → load_json round-trip, and the bytes survive os.fsencode.
  • One silent omission closed. os.walk skipped any directory it could not list. With an unreadable directory inside an untracked nested repository, v0.3.1 reported PASS over a state ID that left that directory out. The walk now raises, and that is BLOCKED.

Regressions

tests/test_path_bytes.py, 11 cases:

  • untracked non-UTF-8 filename;
  • modified tracked non-UTF-8 filename;
  • non-UTF-8 symlink target;
  • the same inside a nested repository;
  • a gate that creates a non-UTF-8 name, under evidence/2 and evidence/3;
  • the emit/load_json round-trip;
  • the UTF-8 equality check against v0.3.1;
  • an unreadable path (in-process);
  • an unreadable file and an unreadable nested directory on disk.

The last two skip as root and run in CI, whose runner is not root.

tests/test_verifier_contract.py: test_install_names_each_release_by_the_commits_it_really_is checks each row of the release table against the repository. The tag must resolve to the named commit, the installer at that commit must pin the named runtime, and that runtime must report the named version.

Verification

On 2e78da6, against a baseline captured on main at 3120337. The baseline gates were green: lint clean, 420 passed / 1 skipped.

  • Local: Ruff clean; 430 passed, 3 skipped (420 + 10 new; the 2 permission cases skip as root); drift: false.
  • Negative controls:
    • Run as a non-root user against the v0.3.1 verifier, 9 of 11 new tests fail. The two that pass are the equality and round-trip pins, which hold on both by design.
    • All 11 pass as non-root on this branch.
    • The doc test fails on the old INSTALL.md and on a table row naming the wrong runtime pin.
  • Unchanged: the invariant block that INV_SHA digests (282f2117…).
  • Judgment rungs attested: task, diff, self-review, requirements, architecture, fresh-review.
  • CI: push run 36208352454 (attempt 1, head 2e78da6): lint, tests-py311, tests-py314 PASS, imported with import-ci. The pull_request run 36208371594 is also green. I could not read the job logs from this session (the log host is blocked by network policy), so the non-root run of the permission cases above is my evidence for those two.
  • Compose (state sha256:0c257351…eb93e2a): 0 blocked required gates, 0 stale, STATE MATCH TRUE, CLEAN CLIMB TRUE, READY FOR HUMAN GATE TRUE.

After this

  1. A 0.3.2 release PR: VERSION, pyproject, and a CHANGELOG entry, still evidence-v3.2.
  2. A pin-move PR, once that release commit is on main.

Tag protection (a v* ruleset) is a separate administrative step and is not part of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1


Generated by Claude Code

The verifier decoded `git status -z` paths and symlink targets as strict
UTF-8. On Linux a filename is any byte string, so one untracked file named
with a stray 0xff byte ended `run`, `baseline` and `attest` in a
UnicodeDecodeError - exit 1, which this tool uses for FAIL - where the state
should have been identified or refused as BLOCKED.

Paths now go through os.fsdecode and os.fsencode. That is the identity on
every valid UTF-8 path, so no state id that could be computed before changes;
a test pins that against the v0.3.1 verifier over non-ASCII names, a rename,
symlinks and a nested repository. An undecodable byte becomes a surrogate
escape that encodes back to the same byte, and a record naming such a path
in drift_paths is written as a JSON escape, ASCII-safe, and loads back
unchanged: the existing serialization is sufficient, as a regression shows.

A path that cannot be read is now BLOCKED naming it rather than a traceback.
This also closes a silent omission: os.walk skipped a directory it could not
list inside an untracked nested repository, so the state id quietly left
part of the tree out. The walk now raises, and that too is BLOCKED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
INSTALL.md said "There are no release tags yet" and that once releases were
cut a tag would be the thing to pin. v0.3.0 and v0.3.1 exist, and a tag here
can still be moved or deleted, so the first sentence was false and the second
would have pointed users at a mutable name.

The pin section now tells users to pin a full commit id, and names each
release by the two commits it really is: the commit its tag resolves to,
whose scripts install it, and the runtime commit those scripts pin. For
0.3.1 that is v0.3.1 -> 84418d8 and runtime 819b7c2. Tags become a pin only
once they are protected against being moved; this change sets no tag policy.

A test checks the table against the repository rather than trusting it: the
tag resolves to the named commit, the installer there pins the named
runtime, and that runtime reports the named version. The invariant block the
installer digests is untouched (INV_SHA 282f2117...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016m6di8xFVdwwq949mJx9U1
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review September 28, 2026 09:12
@Deep-Sixed
Deep-Sixed merged commit e0e4b99 into main Sep 28, 2026
2 checks passed
@Deep-Sixed
Deep-Sixed deleted the claude/relaxed-brown-dzjej0 branch September 28, 2026 09:12
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.

2 participants