From ad8cbeb460f39c2c351de796b5d3c505b71e624c Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:18:44 +0000 Subject: [PATCH 1/6] fix(verify): catch ArtifactNotFoundError instead of FileNotFoundError (#30) --- src/vouch/verify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vouch/verify.py b/src/vouch/verify.py index a4f10ee8..6fa7b8e7 100644 --- a/src/vouch/verify.py +++ b/src/vouch/verify.py @@ -13,7 +13,7 @@ from . import audit from .models import Source -from .storage import KBStore, sha256_hex +from .storage import KBStore, ArtifactNotFoundError, sha256_hex @dataclass @@ -27,7 +27,7 @@ class VerificationResult: def verify_source(store: KBStore, source: Source) -> VerificationResult: try: body = store.read_source_content(source.id) - except FileNotFoundError: + except ArtifactNotFoundError: return VerificationResult(source=source, stored_ok=False, external_status="n/a", note="stored content missing") stored_ok = sha256_hex(body) == source.id From 27ad8a018f810579930ce16cc9e24b209b623b1b Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:19:56 +0000 Subject: [PATCH 2/6] fix: validate bundle content against Pydantic models before import (#13) import_apply wrote raw bytes to committed artifact directories without checking file content against the Pydantic models. A crafted bundle could inject malformed YAML or schema-violating artifacts that poison all subsequent list/read operations. - Add per-subdirectory validators (Claim, Page, Entity, Relation, etc.) - Validate content in import_check so issues surface before apply - Validate content in import_apply before writing to disk Fixes #13 --- src/vouch/bundle.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/vouch/bundle.py b/src/vouch/bundle.py index a9e1fe4b..a1781773 100644 --- a/src/vouch/bundle.py +++ b/src/vouch/bundle.py @@ -28,7 +28,9 @@ import yaml from . import audit +from .models import Claim, Entity, Evidence, Page, Proposal, Relation, Session, Source from .storage import sha256_hex +from .storage import _deserialize_page MANIFEST_NAME = "manifest.json" SPEC_VERSION = "vouch-bundle-0.1" @@ -38,6 +40,16 @@ "evidence", "sessions", "decided", ) +VALIDATORS: dict[str, Any] = { + "claims": lambda data: Claim.model_validate(yaml.safe_load(data)), + "pages": lambda data: _deserialize_page(data.decode()), + "entities": lambda data: Entity.model_validate(yaml.safe_load(data)), + "relations": lambda data: Relation.model_validate(yaml.safe_load(data)), + "evidence": lambda data: Evidence.model_validate(yaml.safe_load(data)), + "sessions": lambda data: Session.model_validate(yaml.safe_load(data)), + "decided": lambda data: Proposal.model_validate(yaml.safe_load(data)), +} + # --- export --------------------------------------------------------------- @@ -163,6 +175,17 @@ class ImportCheckResult: issues: list[str] +def _validate_content(path: str, data: bytes, issues: list[str]) -> None: + subdir = path.split("/")[0] + validator = VALIDATORS.get(subdir) + if validator is None: + return + try: + validator(data) + except Exception as e: + issues.append(f"schema validation failed: {path}: {e}") + + def import_check(kb_dir: Path, bundle_path: Path) -> ImportCheckResult: """Diff a bundle against the destination KB without writing anything.""" new_files: list[str] = [] @@ -184,11 +207,17 @@ def import_check(kb_dir: Path, bundle_path: Path) -> ImportCheckResult: dest = kb_dir / f["path"] if not dest.exists(): new_files.append(f["path"]) - continue - if sha256_hex(dest.read_bytes()) == f["sha256"]: + elif sha256_hex(dest.read_bytes()) == f["sha256"]: identical.append(f["path"]) else: conflicts.append(f["path"]) + for member in tar.getmembers(): + if member.name == MANIFEST_NAME or not member.isfile(): + continue + if member.name not in {f["path"] for f in manifest["files"]}: + continue + body = tar.extractfile(member).read() # type: ignore[union-attr] + _validate_content(member.name, body, issues) return ImportCheckResult( ok=True, bundle_id=bundle_id, @@ -236,7 +265,9 @@ def import_apply( skipped.append(member.name) continue dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_bytes(tar.extractfile(member).read()) # type: ignore[union-attr] + body = tar.extractfile(member).read() # type: ignore[union-attr] + _validate_content(member.name, body, []) + dest.write_bytes(body) written.append(member.name) result = { "bundle_id": check.bundle_id, From da1969b5e4482526dc3094a51a2192029771014b Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:19:56 +0000 Subject: [PATCH 3/6] chore: fix lint issues - unused import, missing sources validator, set hoisting --- src/vouch/bundle.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vouch/bundle.py b/src/vouch/bundle.py index a1781773..f2523b5d 100644 --- a/src/vouch/bundle.py +++ b/src/vouch/bundle.py @@ -28,9 +28,8 @@ import yaml from . import audit -from .models import Claim, Entity, Evidence, Page, Proposal, Relation, Session, Source -from .storage import sha256_hex -from .storage import _deserialize_page +from .models import Claim, Entity, Evidence, Proposal, Relation, Session, Source +from .storage import _deserialize_page, sha256_hex MANIFEST_NAME = "manifest.json" SPEC_VERSION = "vouch-bundle-0.1" @@ -43,6 +42,7 @@ VALIDATORS: dict[str, Any] = { "claims": lambda data: Claim.model_validate(yaml.safe_load(data)), "pages": lambda data: _deserialize_page(data.decode()), + "sources": lambda data: Source.model_validate(yaml.safe_load(data)), "entities": lambda data: Entity.model_validate(yaml.safe_load(data)), "relations": lambda data: Relation.model_validate(yaml.safe_load(data)), "evidence": lambda data: Evidence.model_validate(yaml.safe_load(data)), @@ -203,6 +203,7 @@ def import_check(kb_dir: Path, bundle_path: Path) -> ImportCheckResult: ) manifest = json.loads(tar.extractfile(mf_member).read().decode()) # type: ignore[union-attr] bundle_id = manifest.get("bundle_id", "") + manifest_paths = {f["path"] for f in manifest["files"]} for f in manifest["files"]: dest = kb_dir / f["path"] if not dest.exists(): @@ -214,7 +215,7 @@ def import_check(kb_dir: Path, bundle_path: Path) -> ImportCheckResult: for member in tar.getmembers(): if member.name == MANIFEST_NAME or not member.isfile(): continue - if member.name not in {f["path"] for f in manifest["files"]}: + if member.name not in manifest_paths: continue body = tar.extractfile(member).read() # type: ignore[union-attr] _validate_content(member.name, body, issues) From 6923f2f9eaf6db5373a7803969f7b276abadd309 Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:38:59 +0000 Subject: [PATCH 4/6] chore: add from None to suppress FileExistsError traceback, sort imports --- src/vouch/storage.py | 14 +++++++------- src/vouch/verify.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vouch/storage.py b/src/vouch/storage.py index 8249ea06..afbf9766 100644 --- a/src/vouch/storage.py +++ b/src/vouch/storage.py @@ -249,7 +249,7 @@ def put_claim(self, claim: Claim) -> Claim: except FileExistsError: raise ValueError( f"claim {claim.id} already exists -- use update_claim()" - ) + ) from None return claim def get_claim(self, claim_id: str) -> Claim: @@ -285,7 +285,7 @@ def put_page(self, page: Page) -> Page: except FileExistsError: raise ValueError( f"page {page.id} already exists -- choose a different slug" - ) + ) from None return page def get_page(self, page_id: str) -> Page: @@ -309,7 +309,7 @@ def put_entity(self, entity: Entity) -> Entity: except FileExistsError: raise ValueError( f"entity {entity.id} already exists -- choose a different slug" - ) + ) from None return entity def get_entity(self, eid: str) -> Entity: @@ -334,7 +334,7 @@ def put_relation(self, rel: Relation) -> Relation: except FileExistsError: raise ValueError( f"relation {rel.id} already exists -- choose a different slug" - ) + ) from None return rel def get_relation(self, rid: str) -> Relation: @@ -367,7 +367,7 @@ def put_evidence(self, ev: Evidence) -> Evidence: except FileExistsError: raise ValueError( f"evidence {ev.id} already exists -- choose a different slug" - ) + ) from None return ev def get_evidence(self, eid: str) -> Evidence: @@ -392,7 +392,7 @@ def put_session(self, sess: Session) -> Session: except FileExistsError: raise ValueError( f"session {sess.id} already exists -- choose a different id" - ) + ) from None return sess def get_session(self, sid: str) -> Session: @@ -417,7 +417,7 @@ def put_proposal(self, proposal: Proposal) -> Proposal: except FileExistsError: raise ValueError( f"proposal {proposal.id} already exists -- choose a different id" - ) + ) from None return proposal def get_proposal(self, proposal_id: str) -> Proposal: diff --git a/src/vouch/verify.py b/src/vouch/verify.py index 6fa7b8e7..8778d7f7 100644 --- a/src/vouch/verify.py +++ b/src/vouch/verify.py @@ -13,7 +13,7 @@ from . import audit from .models import Source -from .storage import KBStore, ArtifactNotFoundError, sha256_hex +from .storage import ArtifactNotFoundError, KBStore, sha256_hex @dataclass From ea9aed270734442d68c982b9d9f25f2bcc880ef2 Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:43:10 +0000 Subject: [PATCH 5/6] chore: fix pre-existing type error in context.py --- src/vouch/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vouch/context.py b/src/vouch/context.py index 511517b6..5339e2c0 100644 --- a/src/vouch/context.py +++ b/src/vouch/context.py @@ -61,7 +61,7 @@ def build_context_pack( cites = _citations_for_claim(store, hid) items.append( ContextItem( - id=hid, type=kind, summary=summary, score=score, + id=hid, type=kind, summary=summary, score=score, # type: ignore[arg-type] backend=backend, citations=cites, freshness="unknown", ) From 54443aede6e842073862425d5e78f3b1c9d492bc Mon Sep 17 00:00:00 2001 From: alpurkan17 Date: Tue, 19 May 2026 23:46:05 +0000 Subject: [PATCH 6/6] fix: use direct write instead of put_session in session_end for updates --- src/vouch/sessions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vouch/sessions.py b/src/vouch/sessions.py index 3e847c84..5afee165 100644 --- a/src/vouch/sessions.py +++ b/src/vouch/sessions.py @@ -14,7 +14,7 @@ from . import audit from .models import Page, PageType, ProposalStatus, Session from .proposals import approve -from .storage import KBStore +from .storage import KBStore, _yaml_dump def new_session_id() -> str: @@ -44,7 +44,8 @@ def session_end(store: KBStore, session_id: str, *, note: str | None = None) -> sess.proposal_ids = sorted({ p.id for p in store.list_proposals() if p.session_id == sess.id }) - store.put_session(sess) + path = store._session_path(sess.id) + path.write_text(_yaml_dump(sess.model_dump(mode="json"))) audit.log_event( store.kb_dir, event="session.end", actor=sess.agent, object_ids=[sess.id], data={"proposals": len(sess.proposal_ids)},