Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ All notable changes to vouch are documented here. Format follows
- console Dashboard view: 12-month activity calendar, last-30-days bars,
hour-of-week heatmap, top actors and event mix, driven by `kb.activity`.

### Fixed
- the JSONL / HTTP `/rpc` envelope misreported lifecycle guard failures
(e.g. `kb.supersede` of a claim onto itself) as `internal_error` and
logged a full server-side traceback for what is a bad request —
`LifecycleError` now maps to `invalid_request` like `ProposalError`,
matching how the CLI already surfaces it.

## [1.2.2] — 2026-07-07

### Packaging
Expand Down
8 changes: 7 additions & 1 deletion src/vouch/jsonl_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from . import verify as verify_mod
from .capabilities import capabilities as build_caps
from .context import build_context_pack
from .lifecycle import LifecycleError
from .logging_config import configure_logging
from .models import ProposalStatus
from .page_filters import filter_pages
Expand Down Expand Up @@ -891,7 +892,12 @@ def handle_request(envelope: dict) -> dict:
"id": req_id, "ok": False,
"error": {"code": "missing_param", "message": str(e)},
}
except (ValueError, ProposalError, ArtifactNotFoundError) as e:
except (ValueError, ProposalError, LifecycleError, ArtifactNotFoundError) as e:
# Caller-visible domain errors. LifecycleError belongs here like
# ProposalError does (both are RuntimeError subclasses the generic
# branch would otherwise misreport as internal_error): a lifecycle
# guard such as "a claim cannot supersede itself" is a bad request,
# not a server fault. The CLI already maps it via _cli_errors.
return {
"id": req_id, "ok": False,
"error": {"code": "invalid_request", "message": str(e)},
Expand Down
24 changes: 24 additions & 0 deletions tests/test_jsonl_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,27 @@ def _boom(_params: dict) -> dict:
assert resp["ok"] is False
assert resp["error"]["code"] == "internal_error"
assert "traceback" not in resp["error"]


def test_jsonl_lifecycle_guard_maps_to_invalid_request(
store: KBStore, monkeypatch,
) -> None:
"""Lifecycle guards are caller conditions, not server faults.

`kb.supersede` of a claim onto itself raises LifecycleError, which the
envelope used to misreport as internal_error (and log a full server-side
traceback for what is a bad request). It must map to invalid_request,
exactly as the CLI already surfaces it via _cli_errors."""
monkeypatch.chdir(store.root)
src = store.put_source(b"e")
store.put_claim(Claim(id="c1", text="a durable claim", evidence=[src.id]))
resp = handle_request({
"id": "r1", "method": "kb.supersede",
"params": {"old_claim_id": "c1", "new_claim_id": "c1"},
})
assert resp["ok"] is False
assert resp["error"]["code"] == "invalid_request"
assert "cannot supersede itself" in resp["error"]["message"]
# the guard fired before any write — the claim is untouched
assert store.get_claim("c1").status.value == "working"
assert store.get_claim("c1").superseded_by is None