What happened
On Windows, vouch export writes bundle paths into manifest.json
using the native \ separator while tarfile records member names
with /. Every bundle vouch produces on Windows is therefore
self-invalid:
manifest.counts is {claims: 0, pages: 0, sources: 0, ...} for
every KB regardless of content — the per-subdir counter at
src/vouch/bundle.py:90 uses f["path"].startswith(f"{sub}/"),
which never matches sources\....
vouch export-check on the bundle vouch just produced returns
ok: false with file in bundle but not in manifest for every
tar member and manifest lists missing file for every manifest
entry.
vouch import-apply is a silent no-op: the manifest-paths set
lookup at src/vouch/bundle.py:264 is keyed on the \-flavoured
strings, so every tar member is treated as "not in manifest" and
skipped.
Portable bundles are a load-bearing feature of vouch (README:
"Portable bundles"), so this corrupts the only mechanism for moving
a KB between repos on any Windows host.
What you expected
vouch export produces a bundle whose manifest.json matches the
member names inside the tarball, vouch export-check reports
ok: true, and vouch import-apply restores every artifact —
behaviour identical to Linux/macOS.
Reproduction
Minimum sequence on Windows (PowerShell, Python 3.12, vouch 0.0.1
at commit c3accb6, the merge of #46):
PS> $env:PYTHONUTF8 = '1' # separate Windows bug, needed for `vouch --help`
PS> $env:VOUCH_AGENT = 'repro-bot'
PS> mkdir $env:TEMP\vouch-repro; cd $env:TEMP\vouch-repro
PS> vouch init
Initialised KB at C:\Users\Administrator\AppData\Local\Temp\vouch-repro\.vouch
PS> 'doc body' | Out-File -Encoding utf8 doc.txt
PS> vouch source add doc.txt --title 'Repro doc'
f87b4431a0ad0a4bb9f002bd56cfa9ab07791783684ca3df80be249d434134f4
PS> vouch export --out kb.tar.gz
{
"bundle_id": "bee1189d498a3539b4c695ac7b81b1a386e4d2bff6c68edf481e26e9a409f7c4",
"files": 3,
"out": "kb.tar.gz"
}
PS> vouch export-check kb.tar.gz
{
"bundle_id": "bee1189d...",
"files_checked": 3,
"issues": [
"file in bundle but not in manifest: sources/f87b4431.../content",
"file in bundle but not in manifest: sources/f87b4431.../meta.yaml",
"manifest lists missing file: sources\\f87b4431...\\content",
"manifest lists missing file: sources\\f87b4431...\\meta.yaml"
],
"ok": false
}
The asymmetry between sources/... (tar member) and sources\...
(manifest path) is the root cause.
A round-trip with vouch import-apply into a fresh .vouch/
writes nothing — every member is silently skipped because its
/-shaped name isn't in the manifest's \-shaped path set.
The pytest suite hits the same bug via
tests/test_bundle.py::test_export_import_round_trip and
tests/test_bundle.py::test_import_apply_skips_conflicts_by_default,
both of which fail on Windows but pass on the Linux-only GitHub
Actions matrix (.github/workflows/ci.yml:11 — runs-on: ubuntu-latest).
Environment
.vouch/ state
Not run on the repro KB — the failure is observable from export
/ export-check alone and is independent of any .vouch/
content.
Anything else
Suggested fix
build_manifest and export() should normalise paths to POSIX
when emitting the manifest and the tar arcname. Two small changes
in src/vouch/bundle.py:
@@ src/vouch/bundle.py:73
for rel, abs_path in _iter_export_files(kb_dir):
data = abs_path.read_bytes()
files.append({
- "path": str(rel),
+ "path": rel.as_posix(),
"size": len(data),
"sha256": sha256_hex(data),
})
@@ src/vouch/bundle.py:105
with tarfile.open(dest, "w:gz") as tar:
for rel, abs_path in _iter_export_files(kb_dir):
- tar.add(abs_path, arcname=str(rel))
+ tar.add(abs_path, arcname=rel.as_posix())
POSIX separators are the on-the-wire format anyway: tarfile uses
/, _unsafe_name_reason already checks startswith("/"), and
the startswith(f"{sub}/") counter at line 90 assumes the same.
No existing Linux/macOS bundles change shape; Windows bundles
become valid.
Why it's worth fixing
- Silent data-shape corruption (no exception, just
ok: false on
re-check and zero rows on import) is the highest-risk class of
bug for a tool whose marketing surface includes "portable
bundles" and per-file sha256 manifests.
- Root cause is one method, two lines, one Path API call.
- Adding
windows-latest to the runs-on matrix in
.github/workflows/ci.yml would catch this and several adjacent
Windows bugs (e.g. os.O_NOFOLLOW use in
src/vouch/storage.py:137, and the UnicodeEncodeError on
vouch --help under cp1252 — separate reports).
Checked for duplicates
What happened
On Windows,
vouch exportwrites bundle paths intomanifest.jsonusing the native
\separator whiletarfilerecords member nameswith
/. Every bundle vouch produces on Windows is thereforeself-invalid:
manifest.countsis{claims: 0, pages: 0, sources: 0, ...}forevery KB regardless of content — the per-subdir counter at
src/vouch/bundle.py:90usesf["path"].startswith(f"{sub}/"),which never matches
sources\....vouch export-checkon the bundle vouch just produced returnsok: falsewithfile in bundle but not in manifestfor everytar member and
manifest lists missing filefor every manifestentry.
vouch import-applyis a silent no-op: the manifest-paths setlookup at
src/vouch/bundle.py:264is keyed on the\-flavouredstrings, so every tar member is treated as "not in manifest" and
skipped.
Portable bundles are a load-bearing feature of vouch (README:
"Portable bundles"), so this corrupts the only mechanism for moving
a KB between repos on any Windows host.
What you expected
vouch exportproduces a bundle whosemanifest.jsonmatches themember names inside the tarball,
vouch export-checkreportsok: true, andvouch import-applyrestores every artifact —behaviour identical to Linux/macOS.
Reproduction
Minimum sequence on Windows (PowerShell, Python 3.12, vouch 0.0.1
at commit
c3accb6, the merge of #46):The asymmetry between
sources/...(tar member) andsources\...(manifest path) is the root cause.
A round-trip with
vouch import-applyinto a fresh.vouch/writes nothing — every member is silently skipped because its
/-shaped name isn't in the manifest's\-shaped path set.The pytest suite hits the same bug via
tests/test_bundle.py::test_export_import_round_tripandtests/test_bundle.py::test_import_apply_skips_conflicts_by_default,both of which fail on Windows but pass on the Linux-only GitHub
Actions matrix (
.github/workflows/ci.yml:11—runs-on: ubuntu-latest).Environment
vouch, version 0.0.1(commitc3accb6, on top of fix: enforce forbidden_self_approval gate in proposals.approve() #46)Python 3.12.13(Miniconda3).vouch/stateNot run on the repro KB — the failure is observable from
export/
export-checkalone and is independent of any.vouch/content.
Anything else
Suggested fix
build_manifestandexport()should normalise paths to POSIXwhen emitting the manifest and the tar
arcname. Two small changesin
src/vouch/bundle.py:@@ src/vouch/bundle.py:73 for rel, abs_path in _iter_export_files(kb_dir): data = abs_path.read_bytes() files.append({ - "path": str(rel), + "path": rel.as_posix(), "size": len(data), "sha256": sha256_hex(data), }) @@ src/vouch/bundle.py:105 with tarfile.open(dest, "w:gz") as tar: for rel, abs_path in _iter_export_files(kb_dir): - tar.add(abs_path, arcname=str(rel)) + tar.add(abs_path, arcname=rel.as_posix())POSIX separators are the on-the-wire format anyway: tarfile uses
/,_unsafe_name_reasonalready checksstartswith("/"), andthe
startswith(f"{sub}/")counter at line 90 assumes the same.No existing Linux/macOS bundles change shape; Windows bundles
become valid.
Why it's worth fixing
ok: falseonre-check and zero rows on import) is the highest-risk class of
bug for a tool whose marketing surface includes "portable
bundles" and per-file sha256 manifests.
windows-latestto theruns-onmatrix in.github/workflows/ci.ymlwould catch this and several adjacentWindows bugs (e.g.
os.O_NOFOLLOWuse insrc/vouch/storage.py:137, and theUnicodeEncodeErroronvouch --helpunder cp1252 — separate reports).Checked for duplicates
vouchdev/vouch: nothing mentionsWindows, bundle, path separator,
as_posix, or related terms.vouchdev/vouch: nothing addresses Windowssupport or bundle path encoding (PR fix: validate bundle content against Pydantic models before import (#13) #34 added pydantic
validation to bundle import but did not touch path handling).