fix(kvb-check-gate): skip parent journal rows whose text cannot be encoded

A row with a lone surrogate in gate-parents.jsonl, which sandboxed agents can write, raised UnicodeEncodeError at the TEMP table insert and crashed every commit-msg, pre-push and pre-merge-commit hook and the hub gate view. opened() now keeps only rows whose repo, branch and parent encode; the test fails without the change.

🎯 Fix: 97 %
💥 Break: 2 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 95
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-12 18:09:16 +00:00
1 parent c8b2e586de
commit 9f34768497
2 files changed
+17 -1

No files matched your search

+11 -1
View File
@@ -184,6 +184,16 @@ def journal(origin: Path, kept_dir: Path, kept: Path) -> list[dict]:
return list(merged.values()) return list(merged.values())
def encodable(value: object) -> bool:
if not isinstance(value, str):
return False
try:
value.encode()
except UnicodeEncodeError:
return False
return True
def journal_lock(path: Path) -> contextlib.AbstractContextManager: def journal_lock(path: Path) -> contextlib.AbstractContextManager:
return watch.nofollow.locked(watch.ROOT, path.with_suffix(".lock"), FILE_MODE, None) return watch.nofollow.locked(watch.ROOT, path.with_suffix(".lock"), FILE_MODE, None)
@@ -262,7 +272,7 @@ def opened(path: Path, write: bool, kept_dir: Path) -> sqlite3.Connection:
db.executemany("INSERT INTO recorded VALUES (?, ?, ?)", [ db.executemany("INSERT INTO recorded VALUES (?, ?, ?)", [
(row["repo"], row["branch"], row["parent"]) (row["repo"], row["branch"], row["parent"])
for row in journal(watch.PARENTS, kept_dir, kept_dir / watch.KEPT_PARENTS.name) for row in journal(watch.PARENTS, kept_dir, kept_dir / watch.KEPT_PARENTS.name)
if all(isinstance(row.get(key), str) for key in ("repo", "branch", "parent")) and row["branch"] != row["parent"] if all(encodable(row.get(key)) for key in ("repo", "branch", "parent")) and row["branch"] != row["parent"]
]) ])
except BaseException: except BaseException:
db.close() db.close()
+6
View File
@@ -139,6 +139,12 @@ class TestGates(Scratch):
check = self.gates("receipt", "check", "--repo", repo, "--tree", tree, "--level", "test") check = self.gates("receipt", "check", "--repo", repo, "--tree", tree, "--level", "test")
self.assertEqual((check.returncode, check.stderr), (0, "")) self.assertEqual((check.returncode, check.stderr), (0, ""))
def test_a_parent_row_with_a_lone_surrogate_is_skipped_and_the_others_still_count(self):
self.council.mkdir(exist_ok=True)
rows = [{"id": "bad", "repo": str(self.repo), "branch": "main", "parent": "\ud800"}, {"id": "good", "repo": str(self.repo), "branch": "main", "parent": "feature"}]
(self.council / "gate-parents.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
self.assertEqual(self.parents("main"), [{"parent": "feature", "source": "recorded"}])
def test_a_receipt_older_than_30_days_neither_counts_nor_survives_the_next_write(self): def test_a_receipt_older_than_30_days_neither_counts_nor_survives_the_next_write(self):
repo, tree = str(self.repo), self.out("rev-parse", "HEAD^{tree}") repo, tree = str(self.repo), self.out("rev-parse", "HEAD^{tree}")
old = {"id": "old", "repo": repo, "tree": tree, "level": "test", "gpu": "false", "at": time.time() - 31 * 86400} old = {"id": "old", "repo": repo, "tree": tree, "level": "test", "gpu": "false", "at": time.time() - 31 * 86400}