fix(okf): tolerate embedded-NUL cross-link target — skip, never raise

navigate_bundle's out-of-bundle filter drops every '/'- and '\'-bearing
target before resolution, but a NUL-byte target carries no path separator
and slipped through to (bundle_dir / target).resolve(), which raises
ValueError: embedded null character — propagating instead of being skipped.
method-spec §72 requires a broken cross-link to be tolerated (skipped,
never raised). Wrap resolve/is_file in `except ValueError: continue`;
parse_concept_file stays outside the guard so malformed concept files
still raise.

Load-bearing test drives the first dangerous target THROUGH the filter
into resolution; detach-proved RED (ValueError propagates) when the guard
is removed. Closes the nullbyte item reported OPEN in OKF trinn E.

442→443 green, golden byte-exact, full gate clean (ruff+format+mypy strict).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
This commit is contained in:
Kjell Tore Guttormsen 2026-07-23 22:20:50 +02:00
commit 613b00f882
2 changed files with 27 additions and 2 deletions

View file

@ -112,8 +112,15 @@ def navigate_bundle(bundle_dir: Path) -> list[ConceptFile]:
if "/" in target or "\\" in target or target in seen:
continue
seen.add(target)
resolved = (bundle_dir / target).resolve()
if not resolved.is_relative_to(bundle_root) or not resolved.is_file():
try:
resolved = (bundle_dir / target).resolve()
if not resolved.is_relative_to(bundle_root) or not resolved.is_file():
continue
except ValueError:
# An unrepresentable target (e.g. an embedded NUL byte, which carries no
# path separator and so slips past the out-of-bundle filter) makes
# ``resolve``/``is_file`` raise ``ValueError``. That is a broken link, not
# a fatal error — skip it, never raise (method-spec §72, OKF robustness).
continue
concepts.append(parse_concept_file(resolved))
return concepts