Skip to content

feat(hub): gated federation — push/pull + receiving-side review gate#536

Merged
plind-junior merged 10 commits into
testfrom
feat/vouchhub-gated-import
Jul 21, 2026
Merged

feat(hub): gated federation — push/pull + receiving-side review gate#536
plind-junior merged 10 commits into
testfrom
feat/vouchhub-gated-import

Conversation

@plind-junior

@plind-junior plind-junior commented Jul 21, 2026

Copy link
Copy Markdown
Member

what

vouch federation, end to end and gated. this branch stacks the hub client
(push/pull/link/status, pre-existing on feat/knowledge-bundle-export) with a new
receiving gate so inbound knowledge can never bypass review.

the load-bearing change: before this, every federation receive path
(bundle.import_apply, sync.sync_apply, hub pull) wrote inbound claims straight
into the committed store -- the invariant was upheld only by removing those
methods from agent surfaces. now it is upheld by design.

the three new slices (on top of the hub-client base)

  • gated import (a19beec): bundle.import_as_proposals files each inbound claim as
    a pending proposal via propose_claim; sources/evidence are registered as
    substrate; pages/entities/relations are reported as deferred, never dropped.
    provenance: the proposing actor is hub: and each claim carries an
    origin: tag that survives approval. a per-kb instance id
    (.vouch/instance_id sidecar) is stamped into the bundle manifest as kb_id. hub
    pull is rewired to this path; a new vouch import-proposals cli covers
    file-based gated import.
  • gated sync (95e9390): sync-apply --as-proposals lands another kb's new claims
    as proposals; the direct-write default is unchanged (a human reconciling their
    own kbs, same posture as import-apply). the propose-inbound-claim logic is
    shared so both receive paths stamp identical provenance.
  • read-time provenance (3c8139a): ContextItem.origin plus a pack-level origins
    field, so recalled federated knowledge names which kb vouched for it.

the base commits (pre-existing, not authored here)

b74f8dd..4380920 are the hub-client work (export exclude filter, knowledge-only
preset, hub link/push/pull/status). they are unmerged and unpushed elsewhere, so
they ride along here; the receiving gate depends on them. if you'd rather land
them separately, this branch rebases cleanly once they're in.

tests

new: test_gated_import.py (gated lifecycle, provenance, conformance, instance id,
read-time origin) and gated-sync cases in test_sync.py; the hub pull tests are
rewired to assert proposals instead of committed writes. full suite green, mypy
clean, ruff clean on the authored files.

invariant

any data path that lands writes without a receiving-side proposal is wrong
(roadmap step 10). test_conformance_gate_holds_only_approve_makes_it_durable
locks it: after import the committed claim file does not exist; only approve()
creates it.

Summary by CodeRabbit

  • New Features
    • Added VouchHub linking and synchronization commands (push/pull/status), with token persistence and ETag-based up-to-date checks.
    • Added knowledge-only bundle export/import with --exclude, plus richer export metadata.
    • Added gated proposal-based import and a federation-safe sync-apply --as-proposals mode with origin provenance.
    • Added provenance propagation in context results (origin / origins).
  • Bug Fixes
    • Prevented proposal-based imports/sync from overwriting committed knowledge and from writing excluded artifacts.

the receiving side of vouchhub. inbound bundle knowledge no longer writes
straight to the committed store: bundle.import_as_proposals files each
inbound claim as a pending proposal via propose_claim, so nothing lands
without passing this kb's own proposals.approve() -- the load-bearing
invariant (roadmap step 10) upheld by design, not by surface removal.

- import_as_proposals: sources/evidence registered as substrate (the inbox
  shape), claims filed as pending proposals; pages/entities/relations
  reported as deferred, never silently dropped.
- provenance: the proposing actor is hub:<origin-kb> and each proposed claim
  carries an origin:<kb> tag that survives approval, so a reviewer and the
  durable claim both show which kb vouched.
- kb instance id: a lazy per-kb identity in a .vouch/instance_id sidecar,
  stamped into the bundle manifest as kb_id; kept out of config and every
  exported subdir so identity never travels inside a bundle or collides with
  a receiver's config on import.
- hub pull rewired to the gated path (no on-conflict -- a colliding claim is
  another proposal for the reviewer, not a destructive overwrite); new
  vouch import-proposals cli for file-based gated federation.

tests: test_gated_import.py covers the gated lifecycle, provenance,
conformance (only approve makes it durable), and instance id; the hub pull
tests are rewired to assert proposals instead of committed writes. full
suite green, mypy clean.
closes the second federation receive path that bypassed the review gate.
sync_apply --as-proposals lands another kb's NEW claims as pending proposals
(sources/evidence registered as substrate; pages/entities/relations/sessions/
decided reported as deferred), so nothing reaches the committed store without
passing this kb's own proposals.approve() -- the roadmap step-10 invariant.

the direct-write default is unchanged: like import-apply it stays the human
tool for reconciling your own kbs. --as-proposals (roadmap 8.2) is the
federation-safe path for accepting another kb's knowledge.

the propose-inbound-claim + origin-tag provenance logic is extracted from
bundle.import_as_proposals into shared bundle.propose_inbound_claim and
inbound_claim_id, so both gated receive paths (bundle import and sync) stamp
identical provenance.

tests: gated sync files pending not committed, approves into a claim carrying
the origin tag, and the cli --as-proposals flow. full suite green, mypy clean.
a claim imported through the gate carries an origin:<kb> tag; build_context_pack
now reads it onto each returned item (ContextItem.origin) and lists every
contributing kb in the pack's origins field, so a reader can see which knowledge
came from another kb and which is local -- the read-time half of "every result
names the kb that vouched for it" (roadmap step 10). locally-authored claims
have origin=None and add no origins field.

tests: a federated claim surfaces its origin on recall; a local claim does not.
full suite green, mypy clean.
@github-actions github-actions Bot added cli command line interface mcp mcp, jsonl, and http surfaces storage kb storage, migrations, schemas, and proposals retrieval context, search, synthesis, and evaluation sync sync, vault mirror, and diff flows tests tests and fixtures labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This change adds filtered bundle manifests, KB instance identity, proposal-gated bundle and sync imports, provenance-aware context results, VouchHub push/pull/status commands, and corresponding CLI, JSONL, and test coverage.

Changes

Federated bundle synchronization

Layer / File(s) Summary
Filtered bundle export and validation
src/vouch/bundle.py, src/vouch/cli.py, src/vouch/jsonl_server.py, tests/test_bundle.py
Exports support validated exclusions, KB IDs, per-directory counts, excluded metadata, and expanded export-check results.
Proposal-gated import and provenance
src/vouch/bundle.py, src/vouch/storage.py, src/vouch/context.py, src/vouch/models.py, schemas/*.json, tests/test_gated_import.py
Bundles register substrate first, stage claims as pending proposals, preserve origin metadata, and expose origins in context results.
Proposal-based sync apply
src/vouch/sync.py, src/vouch/cli.py, tests/test_sync.py
sync-apply can stage inbound claims as pending proposals while deferring other durable artifacts.
Hub push, pull, and status workflow
src/vouch/hub_client.py, src/vouch/cli.py, tests/test_hub_client.py
Hub links, tokens, authenticated bundle transfers, conflicts, ETags, and gated pulls are implemented and tested.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant HubClient
  participant Bundle
  participant VouchHub
  CLI->>HubClient: run hub push or pull
  HubClient->>Bundle: export or import bundle
  HubClient->>VouchHub: authenticated PUT or GET
  VouchHub-->>HubClient: bundle, ETag, or conflict
  HubClient-->>CLI: return synchronization result
Loading

Possibly related PRs

Suggested labels: packaging

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main theme: hub federation with push/pull and a receiving-side review gate.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vouchhub-gated-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the size: XL 1000 or more changed non-doc lines label Jul 21, 2026

@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: 1

🤖 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 804-809: Update the source import logic in src/vouch/bundle.py
lines 804-809 and the corresponding sync logic in src/vouch/sync.py lines
332-336 to load each source’s sibling meta.yaml alongside sources/<sha>/content,
preserve its title, locator, url, tags, and metadata fields, and pass those
values to store.put_source(...); apply the same metadata-preserving behavior at
both sites.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e83bfbf-abad-48bb-8829-9a8b7c6b5f89

📥 Commits

Reviewing files that changed from the base of the PR and between 4514a39 and 3c8139a.

📒 Files selected for processing (12)
  • src/vouch/bundle.py
  • src/vouch/cli.py
  • src/vouch/context.py
  • src/vouch/hub_client.py
  • src/vouch/jsonl_server.py
  • src/vouch/models.py
  • src/vouch/storage.py
  • src/vouch/sync.py
  • tests/test_bundle.py
  • tests/test_gated_import.py
  • tests/test_hub_client.py
  • tests/test_sync.py

Comment thread src/vouch/bundle.py
Comment on lines +804 to +809
if m.name.startswith("sources/") and m.name.endswith("/content"):
sha = m.name.split("/")[1]
contents[sha] = tar.extractfile(m).read() # type: ignore[union-attr]
for _sha, body in sorted(contents.items()):
store.put_source(body)
sources_registered += 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# inspect the relevant file layouts and symbols
git ls-files src/vouch/bundle.py src/vouch/sync.py src/vouch/storage.py

echo
echo "== bundle.py outline =="
ast-grep outline src/vouch/bundle.py --view expanded || true

echo
echo "== sync.py outline =="
ast-grep outline src/vouch/sync.py --view expanded || true

echo
echo "== storage.py outline =="
ast-grep outline src/vouch/storage.py --view expanded || true

echo
echo "== relevant call sites =="
rg -n "put_source\(" src/vouch/bundle.py src/vouch/sync.py src/vouch/storage.py
rg -n "meta\.yaml|sources/" src/vouch/bundle.py src/vouch/sync.py src/vouch/storage.py

Repository: vouchdev/vouch

Length of output: 10332


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== storage.py put_source/get_source/list_sources =="
sed -n '338,410p' src/vouch/storage.py

echo
echo "== bundle.py import_apply and import_as_proposals =="
sed -n '611,845p' src/vouch/bundle.py

echo
echo "== sync.py bundle-source path =="
sed -n '150,360p' src/vouch/sync.py

echo
echo "== source model definition =="
rg -n "^class Source\\b|^class Source\\(" src/vouch -g '*.py'

Repository: vouchdev/vouch

Length of output: 21037


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== artifact kind classification =="
sed -n '60,125p' src/vouch/sync.py

echo
echo "== source model =="
sed -n '163,230p' src/vouch/models.py

echo
echo "== bundle source export/import docs =="
sed -n '285,335p' src/vouch/bundle.py
sed -n '490,540p' src/vouch/bundle.py

Repository: vouchdev/vouch

Length of output: 8526


preserve source metadata on gated import

both src/vouch/bundle.py#L804-L809 and src/vouch/sync.py#L332-L336 only persist sources/<sha>/content, so sources/<sha>/meta.yaml fields (title, locator, url, tags, metadata) are dropped on first import. if those fields should survive federation, load the sibling meta file here too and pass its values into store.put_source(...).

📍 Affects 2 files
  • src/vouch/bundle.py#L804-L809 (this comment)
  • src/vouch/sync.py#L332-L336
🤖 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 804 - 809, Update the source import logic
in src/vouch/bundle.py lines 804-809 and the corresponding sync logic in
src/vouch/sync.py lines 332-336 to load each source’s sibling meta.yaml
alongside sources/<sha>/content, preserve its title, locator, url, tags, and
metadata fields, and pass those values to store.put_source(...); apply the same
metadata-preserving behavior at both sites.

resolve the conflicts from the federation gate meeting the test branch's
identity, prompt-gate and personal-kb work:

- bundle.py: union both feature sets — the exclude filter + instance-id
  kb_id provenance (this branch) and identity-stripped config export +
  fenced_bundle_path + kb identity block (test). export now fences the
  dest, strips config identity, forwards exclude, and stamps both kb_id
  and kb; _yaml_dump drops its dead-code marker since test now calls it.
- cli.py: the two branches each added a `hub` command group; a second
  @cli.group() silently shadowed the first. fold hub link/push/pull/status
  onto the single hub group so test's register/list/init-personal/fallback
  subcommands survive alongside them.
- context.py: keep both result blocks — federated `origins` and the
  retrieval-honesty `retrieval` block.
- jsonl_server.py: export handler fences the path (test) and forwards the
  exclude filter (this branch).

also clear pre-existing ruff debt in the hub-client base commits so the
now-computable merge passes ci (conflicts had blocked pull_request ci from
running at all): line length, dead noqa directives, mutable class defaults
via ClassVar, and a redundant session_split import surfaced by the merge.
…ield

the gated-import work added ContextItem.origin (and the pack-level origins)
but the generated json schemas were never regenerated — the drift was latent
because the conflict blocked schema-check from running on the pr. regenerate
context-item and context-pack from models.py so the drift gate passes.
@github-actions github-actions Bot added schemas json schemas and generated schema assets ci: passing ci is green labels Jul 21, 2026

@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/bundle.py (1)

126-167: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

export() re-walks and re-reads every file already processed by build_manifest.

build_manifest (Line 131-132) iterates _iter_export_files and computes _export_file_bytes/sha256_hex for every file, then export() (Line 195, 198-201) walks the same file set again and recomputes _export_file_bytes for the tar. Every exported file is read from disk twice per export call. For a KB with many claims/pages/sources this doubles I/O on a path that is otherwise carefully optimized (see the comment about hash-based bundle IDs).

Consider having build_manifest return (or accept) the already-computed (rel, data) pairs so export() reuses them instead of re-reading from disk.

♻️ Suggested direction
-def build_manifest(
-    kb_dir: Path, exclude: tuple[str, ...] = (), *, kb_id: str | None = None
-) -> dict[str, Any]:
+def build_manifest(
+    kb_dir: Path, exclude: tuple[str, ...] = (), *, kb_id: str | None = None,
+    file_data: dict[str, bytes] | None = None,
+) -> dict[str, Any]:
     exclude = _check_exclude(exclude)
     files: list[dict[str, Any]] = []
     for rel, abs_path in _iter_export_files(kb_dir, exclude):
         data = _export_file_bytes(rel, abs_path)
+        if file_data is not None:
+            file_data[rel.as_posix()] = data
         files.append(...)

Then export() passes a dict in, and reuses it when writing tar members instead of calling _export_file_bytes again.

Also applies to: 187-216

🤖 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 126 - 167, Refactor the
build_manifest/export flow so each export file is read only once: have
build_manifest expose or accept the computed (rel, data) pairs, then update
export() to pass and reuse those bytes when writing tar members instead of
calling _export_file_bytes again. Preserve the existing manifest hashes, paths,
counts, and archive contents.
🤖 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/bundle.py`:
- Around line 126-167: Refactor the build_manifest/export flow so each export
file is read only once: have build_manifest expose or accept the computed (rel,
data) pairs, then update export() to pass and reuse those bytes when writing tar
members instead of calling _export_file_bytes again. Preserve the existing
manifest hashes, paths, counts, and archive contents.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 79629c02-bcfa-44c8-878f-b761843b5b46

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8139a and 16c8848.

📒 Files selected for processing (11)
  • schemas/context-item.schema.json
  • schemas/context-pack.schema.json
  • src/vouch/bundle.py
  • src/vouch/cli.py
  • src/vouch/context.py
  • src/vouch/hub_client.py
  • src/vouch/jsonl_server.py
  • src/vouch/models.py
  • src/vouch/storage.py
  • tests/test_bundle.py
  • tests/test_hub_client.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/vouch/models.py
  • src/vouch/context.py
  • src/vouch/jsonl_server.py
  • tests/test_bundle.py
  • tests/test_hub_client.py
  • src/vouch/storage.py

@plind-junior
plind-junior merged commit 1c9ca7c into test Jul 21, 2026
20 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: passing ci is green cli command line interface mcp mcp, jsonl, and http surfaces retrieval context, search, synthesis, and evaluation schemas json schemas and generated schema assets size: XL 1000 or more changed non-doc lines storage kb storage, migrations, schemas, and proposals sync sync, vault mirror, and diff flows tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant