Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,59 @@ All notable changes to vouch are documented here. Format follows
## [Unreleased]

### Added
- **KB instance identity**: `vouch init` mints a durable id (uuid, stored
in `config.yaml` under `kb:` next to a display name) and stamps it onto
every new audit event and bundle manifest, so history and exported
artifacts stay attributable to the KB that produced them once knowledge
starts moving between KBs. existing KBs are backfilled on re-init or
`vouch hub register` (an additive `kb.identity` audit event — never a
history rewrite); pre-identity audit chains still verify. bundle
imports move settings, never identity: the destination's `kb:` block
survives even an overwrite-import, and same-settings configs no longer
read as conflicts just because ids differ (config.yaml is compared
structurally, modulo `kb:`, on both sides). compat note: a *pre-identity*
vouch importing a new bundle uses the old byte-compare and may report
config.yaml as a conflict — its default skip mode leaves the file
untouched, so nothing breaks; upgrade the importer to converge.
- **machine registry** (`vouch hub register / list / unregister`): a
machine-local list of known KBs at `~/.config/vouch/registry.yaml`
(honours `XDG_CONFIG_HOME`; override with `VOUCH_REGISTRY_PATH`) with a
role per KB — `project`, `personal`, or `team`. advisory routing state
only: authority stays in each KB's own `.vouch/`, and a missing or
corrupt registry degrades to per-project behaviour. this is the
substrate for global (install-once) vouch and the local seed of the
vouchhub registry of connected KBs.
- **scope stamped at write time**: every new claim and page proposal — and
every captured session-answer source — records the KB's own project scope
at the propose gate, so knowledge knows which project it belongs to
before KBs ever start sharing artifacts (scope cannot be retrofitted
later). the stamp and the read-side viewer resolve through ONE chain
(`VOUCH_PROJECT` > `retrieval.scope` > the durable `kb.id`), so what a KB
writes it can always read back — the mutable `kb.name` is never
load-bearing for visibility, and a rename cannot orphan stamped
knowledge. pages join claims and sources as scoped kinds, closing a
cross-KB leak channel (vault edits carry the durable page's scope
through, never a restamp; hand-edited legacy `scope:` frontmatter
degrades to unscoped instead of breaking the page). malformed explicit
scopes are refused at the gate, and a malformed scope already on disk
degrades to unscoped instead of crashing the audit read path. the
SessionStart digest (`vouch recall`) is viewer-filtered like every other
retrieval surface — it used to inject every live claim regardless of
scope — and reports on stderr how many artifacts scope filtering hid,
never filtering silently; the salience sidebar honours the same filter.
existing unscoped artifacts behave exactly as before. explicit `scope=`
overrides are accepted by `propose_claim` / `propose_quoted_claim` /
`propose_page`.
- **hijack-proof KB resolution**: the upward `.vouch` walk never ascends
past `$HOME` any more, so a stray home-directory KB can no longer
silently capture every project below it (a recorded incident class).
starting *in* `$HOME` still resolves its KB; `VOUCH_KB_PATH` and a
`global: {allow_home_kb: true}` opt-in in the home KB's config remain
deliberate escape hatches. a registry entry with role `personal` adds a
second belt: ambient capture into it from another directory is refused
(reads warn). host adapters can now pin the walk's start with
`VOUCH_PROJECT_DIR`; `vouch discover` and `vouch status` report the
resolution chain (`why`) and the KB's id/name.
- the per-prompt recall hook (`vouch context-hook`) is now instructional
and always visible: with relevant approved items it tells the model to
open its reply with **"From vouch memory:"** and ground in the cited
Expand Down
12 changes: 12 additions & 0 deletions schemas/audit-event.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@
"title": "Id",
"type": "string"
},
"kb_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Kb Id"
},
"object_ids": {
"items": {
"type": "string"
Expand Down
49 changes: 49 additions & 0 deletions schemas/page.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
{
"$defs": {
"ArtifactScope": {
"description": "Structured scope: visibility tier plus optional project/agent binding.",
"properties": {
"agent": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Agent"
},
"project": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Project"
},
"visibility": {
"$ref": "#/$defs/Visibility",
"default": "project"
}
},
"title": "ArtifactScope",
"type": "object"
},
"PageStatus": {
"enum": [
"draft",
Expand All @@ -9,6 +44,17 @@
],
"title": "PageStatus",
"type": "string"
},
"Visibility": {
"description": "How widely an artifact is visible within retrieval surfaces.",
"enum": [
"private",
"project",
"team",
"public"
],
"title": "Visibility",
"type": "string"
}
},
"$id": "https://vouch.dev/schemas/page.schema.json",
Expand Down Expand Up @@ -48,6 +94,9 @@
"title": "Metadata",
"type": "object"
},
"scope": {
"$ref": "#/$defs/ArtifactScope"
},
"sources": {
"items": {
"type": "string"
Expand Down
24 changes: 24 additions & 0 deletions src/vouch/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

import yaml

from .models import AuditEvent

if TYPE_CHECKING:
Expand Down Expand Up @@ -75,6 +77,27 @@ def new_event_id() -> str:
return uuid.uuid4().hex


def _kb_id(kb_dir: Path) -> str | None:
"""Best-effort `kb.id` from the KB's config.yaml (None on any problem).

Stamped onto every new event so history stays attributable to its KB of
origin once bundles move artifacts between KBs. A config read failure
must never block an audit append — events fall back to kb_id=None,
exactly like pre-identity history.
"""
try:
loaded = yaml.safe_load((kb_dir / "config.yaml").read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError):
return None
if not isinstance(loaded, dict):
return None
kb = loaded.get("kb")
if not isinstance(kb, dict):
return None
raw = kb.get("id")
return str(raw) if raw else None


def _canonical_json(payload: dict[str, Any]) -> str:
return json.dumps(payload, separators=(",", ":"), sort_keys=True)

Expand Down Expand Up @@ -135,6 +158,7 @@ def log_event(
dry_run=dry_run,
reversible=reversible,
data=data or {},
kb_id=_kb_id(kb_dir),
prev_hash=prev_hash,
)
ev.hash = _compute_hash(prev_hash, _event_payload_for_hash(ev))
Expand Down
Loading
Loading