Skip to content

fix: validate bundle content against Pydantic models before import (#13)#34

Merged
plind-junior merged 7 commits into
vouchdev:mainfrom
alpurkan17:fix/bundle-validation-13
May 22, 2026
Merged

fix: validate bundle content against Pydantic models before import (#13)#34
plind-junior merged 7 commits into
vouchdev:mainfrom
alpurkan17:fix/bundle-validation-13

Conversation

@alpurkan17

@alpurkan17 alpurkan17 commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

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.

Changes

  • Added per-subdirectory validators (Claim, Page, Entity, Relation, Evidence, Session, Proposal)
  • import_check now validates content — issues surface before apply
  • import_apply validates content before writing to disk

Fixes #13

Summary by CodeRabbit

  • New Features

    • Bundle import now validates member contents against their schemas during both check and apply, surfacing validation issues before files are written.
  • Bug Fixes

    • More reliable detection and reporting of missing stored source content.
    • Session updates are now persisted reliably at session end.
  • Chores

    • Storage operations raise clearer errors when an artifact already exists.
    • Minor type-check annotation added to suppress spurious warnings.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds schema-aware validation for bundle members during import_check and import_apply, suppresses exception chaining in KBStore put_* handlers, changes verify_source to catch ArtifactNotFoundError, and updates session persistence and a type-check annotation.

Changes

Bundle validation and artifact error handling

Layer / File(s) Summary
Validation infrastructure and helpers
src/vouch/bundle.py
Imports data models and helpers (Claim, Entity, Evidence, Page via _deserialize_page, Proposal, Relation, Session, Source, sha256_hex, _deserialize_page), defines VALIDATORS keyed by top-level bundle subdirectory, and adds _validate_content(path, data, issues) to run the appropriate validator and append errors to issues.
Manifest-aware validation in import_check
src/vouch/bundle.py
import_check now extracts bytes for each manifest-listed member and calls _validate_content(..., issues) to accumulate schema/decoding issues while keeping existing new/identical/conflict hash logic.
Validated byte writing in import_apply
src/vouch/bundle.py
import_apply now reads each manifest-listed member into body, calls _validate_content(..., []) (issues discarded), then writes body to the destination instead of writing raw extracted bytes without validation.
Suppress exception chaining in KBStore put_ handlers*
src/vouch/storage.py
All file-backed put_* methods (put_claim, put_page, put_entity, put_relation, put_evidence, put_session, put_proposal) now raise ValueError(... ) from None instead of re-raising FileExistsError, suppressing exception chaining.
ArtifactNotFoundError handling in verify_source
src/vouch/verify.py
verify_source now catches ArtifactNotFoundError from storage when stored source content is missing; imports updated to include ArtifactNotFoundError alongside KBStore and sha256_hex.
Context type-check ignore
src/vouch/context.py
Adds # type: ignore[arg-type] to ContextItem(id=hid) to silence a type-checker complaint; no runtime change.
Session persistence: write YAML directly
src/vouch/sessions.py
session_end now writes _yaml_dump(sess.model_dump(mode="json")) to store._session_path(sess.id) instead of calling store.put_session(sess).

Sequence Diagram

sequenceDiagram
  participant Tar as TarArchive
  participant ImportCheck as import_check
  participant Validator as _validate_content
  participant Registry as VALIDATORS[subdir]
  participant ImportApply as import_apply
  participant Dest as Destination

  Tar->>ImportCheck: iterate manifest-listed files
  loop per member
    ImportCheck->>ImportCheck: body = tar.extractfile(member).read()
    ImportCheck->>Validator: _validate_content(path, body, issues)
    Validator->>Registry: select validator by top-level subdir
    Registry->>Validator: validate(body) / decode page
    alt validation fails
      Validator->>ImportCheck: append issue to issues
    else validation succeeds
      Validator->>ImportCheck: no issue
    end
  end

  Tar->>ImportApply: iterate manifest-listed files
  loop per member
    ImportApply->>ImportApply: body = tar.extractfile(member).read()
    ImportApply->>Validator: _validate_content(path, body, [])
    ImportApply->>Dest: write validated body to destination
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #30: verify: replace FileNotFoundError with ArtifactNotFoundError — This PR updates verify_source to catch ArtifactNotFoundError, directly addressing that issue's objective.
  • #13: Bundle import writes unvalidated content to committed artifact directories — This PR implements content validation in import_check and import_apply, addressing the content-validation objective described in the issue.

Possibly related PRs

  • vouchdev/vouch#26: Modifies KBStore put_* methods' FileExistsError handling; related to the suppressed-exception-chaining adjustments here.

Poem

🐰 I hopped through tar and bytes today,
I sniffed each page and YAML play,
I parsed and validated with care,
No malformed files will linger there —
Validators guard the import way.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Changes in storage.py, verify.py, context.py, and sessions.py appear to be related supporting fixes rather than core bundle validation scope. Clarify whether exception chaining suppression, error type updates, type annotations, and session persistence changes are prerequisites for bundle validation or separate concerns.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding schema validation for bundle content during import.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #13: per-subdirectory validators, content validation in import_check before apply, and validation in import_apply before writing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/vouch/bundle.py (2)

214-225: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mark import_check as failed when validation adds issues.

Line 223 hard-codes ok=True, so schema-invalid bundles still look successful to callers even after Line 220 records validation failures.

Suggested fix
     return ImportCheckResult(
-        ok=True, bundle_id=bundle_id,
+        ok=not issues, bundle_id=bundle_id,
         new_files=new_files, conflicts=conflicts,
         identical=identical, issues=issues,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/bundle.py` around lines 214 - 225, The return currently hardcodes
ok=True in ImportCheckResult making schema-invalid bundles appear successful;
change it so ok reflects whether any validation issues were recorded (e.g., ok =
len(issues) == 0 or ok = not issues) in the import_check flow. Update the return
that constructs ImportCheckResult (use the existing issues variable) so ok is
false when _validate_content appended problems, keeping the rest of fields
(bundle_id, new_files, conflicts, identical, issues) unchanged.

243-245: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not write members after validation has failed.

import_apply ignores check.issues, and Line 269 discards any per-member validation failures by passing a throwaway list. Line 270 then writes the invalid bytes anyway, so the bug this PR is fixing still reproduces during apply.

Suggested fix
     check = import_check(kb_dir, bundle_path)
+    if check.issues:
+        raise RuntimeError(f"refusing to import invalid bundle: {'; '.join(check.issues)}")
     if on_conflict == "fail" and check.conflicts:
         raise RuntimeError(f"refusing to import: {len(check.conflicts)} conflicts")
@@
             dest.parent.mkdir(parents=True, exist_ok=True)
             body = tar.extractfile(member).read()  # type: ignore[union-attr]
-            _validate_content(member.name, body, [])
+            member_issues: list[str] = []
+            _validate_content(member.name, body, member_issues)
+            if member_issues:
+                skipped.append(member.name)
+                continue
             dest.write_bytes(body)

Also applies to: 268-270

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/bundle.py` around lines 243 - 245, The import path currently calls
import_check(kb_dir, bundle_path) but then ignores per-member validation
failures (check.issues) and proceeds to write bytes in import_apply; update
import_apply to respect check.issues by (1) collecting and passing the real
issues list returned from import_check into the validation/writer logic instead
of a throwaway list, (2) if check.issues is non-empty and on_conflict == "fail"
abort the apply and raise a clear error (or propagate the RuntimeError used
earlier), and (3) prevent any write of invalid members when validation failures
exist; reference the functions import_check, import_apply and the check.issues
field when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vouch/bundle.py`:
- Around line 43-51: VALIDATORS currently omits a validator for "sources", so
files under sources/* bypass schema checks; add an entry for "sources" mapping
to a validator that parses and validates using the Source model (e.g.,
"sources": lambda data: Source.model_validate(yaml.safe_load(data)) or analogous
deserializer used by other entries) so that the Source import is actually used
and malformed source artifacts are rejected during bundle processing; update
VALIDATORS to include this "sources" key referencing the Source model validator.
- Around line 31-33: Remove the unused Page import from the models import list
to satisfy Ruff and keep imports tidy: change the from .models import line to
only import Claim, Entity, Evidence, Proposal, Relation, Session, Source (remove
Page) and combine the two storage imports into a single line importing
sha256_hex and _deserialize_page from .storage; update references if any rely on
Page but note _deserialize_page is the validator used.

In `@src/vouch/verify.py`:
- Line 16: The import list in the statement importing from .storage is unsorted
and must be alphabetized to satisfy Ruff I001; reorder the imported members so
they are in alphabetical order (e.g., ArtifactNotFoundError, KBStore,
sha256_hex) in the from .storage import ... line while keeping the same imported
symbols (ArtifactNotFoundError, KBStore, sha256_hex) so the names used by
functions/classes in this module are unchanged.

---

Outside diff comments:
In `@src/vouch/bundle.py`:
- Around line 214-225: The return currently hardcodes ok=True in
ImportCheckResult making schema-invalid bundles appear successful; change it so
ok reflects whether any validation issues were recorded (e.g., ok = len(issues)
== 0 or ok = not issues) in the import_check flow. Update the return that
constructs ImportCheckResult (use the existing issues variable) so ok is false
when _validate_content appended problems, keeping the rest of fields (bundle_id,
new_files, conflicts, identical, issues) unchanged.
- Around line 243-245: The import path currently calls import_check(kb_dir,
bundle_path) but then ignores per-member validation failures (check.issues) and
proceeds to write bytes in import_apply; update import_apply to respect
check.issues by (1) collecting and passing the real issues list returned from
import_check into the validation/writer logic instead of a throwaway list, (2)
if check.issues is non-empty and on_conflict == "fail" abort the apply and raise
a clear error (or propagate the RuntimeError used earlier), and (3) prevent any
write of invalid members when validation failures exist; reference the functions
import_check, import_apply and the check.issues field when making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d0227838-0598-423f-b712-1754cf82fc8a

📥 Commits

Reviewing files that changed from the base of the PR and between abf9c8f and 27ad8a0.

📒 Files selected for processing (2)
  • src/vouch/bundle.py
  • src/vouch/verify.py

Comment thread src/vouch/bundle.py Outdated
Comment thread src/vouch/bundle.py
Comment thread src/vouch/verify.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/vouch/sessions.py (1)

17-17: ⚡ Quick win

Keep session persistence behind KBStore's public API.

Importing _yaml_dump and calling store._session_path(...) couple session_end to storage internals, so any later change to write semantics or layout now has to be duplicated here too. Please expose an overwrite/update method on KBStore and call that instead.

♻️ Proposed direction
-from .storage import KBStore, _yaml_dump
+from .storage import KBStore
...
-    path = store._session_path(sess.id)
-    path.write_text(_yaml_dump(sess.model_dump(mode="json")))
+    store.update_session(sess)

Then implement KBStore.update_session(...) in .storage using the canonical session-write path.

Also applies to: 47-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/sessions.py` at line 17, session_end currently imports and uses the
internal _yaml_dump and store._session_path, coupling session persistence to
storage internals; add a public KBStore.update_session(session_id, data,
mode=...) (or similar signature) in storage and replace calls to _yaml_dump and
store._session_path inside session_end with a single call to
store.update_session(...) so the KBStore implementation owns the session write
semantics; update any other places that use store._session_path/_yaml_dump
(e.g., lines 47-48) to call the new KBStore.update_session API instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/vouch/sessions.py`:
- Line 17: session_end currently imports and uses the internal _yaml_dump and
store._session_path, coupling session persistence to storage internals; add a
public KBStore.update_session(session_id, data, mode=...) (or similar signature)
in storage and replace calls to _yaml_dump and store._session_path inside
session_end with a single call to store.update_session(...) so the KBStore
implementation owns the session write semantics; update any other places that
use store._session_path/_yaml_dump (e.g., lines 47-48) to call the new
KBStore.update_session API instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2661a456-0377-4627-ba85-88cc81747957

📥 Commits

Reviewing files that changed from the base of the PR and between c6ee175 and 61958e9.

📒 Files selected for processing (1)
  • src/vouch/sessions.py

Fix: import_check ok=not issues, import_apply block bad writes
_validate_content now only processes .yaml, .yml, .md files
to avoid failing on raw binary data like sources/*/content
@plind-junior

Copy link
Copy Markdown
Collaborator

Hey — I think this one's already done? The same fix is on main as commit 27ad8a0, and the verify.py change went in via #33. That's why it's showing conflicts.

Could you rebase and see if there's anything left? If the diff is empty just close it out and point at 27ad8a0 so it's easy to find later. If there's still stuff you wanted to add, update the description so we know what's new and force-push the rebased branch.

Thanks!

@plind-junior

Copy link
Copy Markdown
Collaborator

Fix the conflict or it will be closed in 12hr

# Conflicts:
#	src/vouch/bundle.py
@plind-junior plind-junior merged commit c56a650 into vouchdev:main May 22, 2026
5 checks passed
plind-junior added a commit that referenced this pull request May 22, 2026
fix: validate bundle content against Pydantic models before import (#13)
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.

Bundle import writes unvalidated content to committed artifact directories

2 participants