Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c31e083
test(calendar): define clean Naruon projection contract
seonghobae Aug 21, 2026
924e868
test(http): require bounded JSON response reads
seonghobae Aug 21, 2026
5282dc6
fix(http): bound admitted JSON response bodies
seonghobae Aug 21, 2026
dc42c3a
feat(calendar): add bounded Naruon read projection
seonghobae Aug 21, 2026
1e54243
test(calendar): cover transport and numeric hardening
seonghobae Aug 21, 2026
796b758
docs(contract): add strict Naruon calendar schema
seonghobae Aug 21, 2026
ccdbdbf
feat(calendar): export Naruon projection package API
seonghobae Aug 21, 2026
0aa9b8f
docs(adr): correct pseudo-CalDAV product language
seonghobae Aug 21, 2026
2914b8b
docs(adr): record clean Naruon calendar authority boundary
seonghobae Aug 21, 2026
c25c5de
docs(doctoring): trace calendar contract standards
seonghobae Aug 21, 2026
da1d029
docs(changelog): add clean calendar projection fragment
seonghobae Aug 21, 2026
10bdeb0
test(http): cover bounded response edge branches
seonghobae Aug 21, 2026
1a22148
test(calendar): cover RFC3339 defensive branches
seonghobae Aug 21, 2026
abd2401
docs(plan): add calendar contract TDD plan
seonghobae Aug 21, 2026
d808690
test(calendar): reject surrounding contract whitespace
seonghobae Aug 21, 2026
fd614aa
fix(calendar): reject silent contract string normalization
seonghobae Aug 21, 2026
364dd66
docs(contract): align exact string whitespace rules
seonghobae Aug 21, 2026
cc54f8c
test(http): require exact JSON response media type
seonghobae Aug 21, 2026
51f10d0
fix(http): validate exact response media types
seonghobae Aug 21, 2026
8156ed8
fix(calendar): require exact provider response media type
seonghobae Aug 21, 2026
72c3fa1
test(calendar): assert response media-type enforcement
seonghobae Aug 21, 2026
de0b453
docs(contract): add calendar consumer conformance fixture
seonghobae Aug 21, 2026
7fa401d
test(calendar): pin consumer conformance fixture digest
seonghobae Aug 21, 2026
0360fea
fix: contain raw provider failures
seonghobae Aug 21, 2026
b566c6d
fix: preserve login return URL and guard admin token
seonghobae Aug 21, 2026
b606c25
fix: degrade malformed chat provider replies (#379)
seonghobae Aug 21, 2026
26f25ac
fix(docs,worker): free ADR 0123 collision and stop mislabeling empty-…
seonghobae Aug 23, 2026
de7f78c
docs(adr): assign Naruon calendar ADR 0145
seonghobae Aug 23, 2026
dd463ed
Revert "fix: preserve login return URL and guard admin token"
seonghobae Aug 23, 2026
b4d94cd
test: prove bounded calendar transport edges
seonghobae Aug 23, 2026
6fc22a9
Reapply "fix: preserve login return URL and guard admin token"
seonghobae Aug 23, 2026
2f3f308
fix(worker): restore diagnostic log for empty-body ingestion skip
seonghobae Aug 24, 2026
4086138
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 24, 2026
0f86e66
fix(docs): renumber Naruon calendar ADR from 0145 to 0168
seonghobae Aug 24, 2026
24448af
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 24, 2026
569e8ae
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 24, 2026
3d91091
Merge remote-tracking branch 'origin/main' into feat/naruon-calendar-…
seonghobae Aug 24, 2026
5be1e09
fix(docs): free pre-existing ADR 0168 collision on main
seonghobae Aug 24, 2026
15d5af2
Merge remote-tracking branch 'origin/main' into codex/pr355-repair
seonghobae Aug 24, 2026
83145dd
fix(calendar): reconcile current main review findings
seonghobae Aug 24, 2026
0a2a72c
fix(ingestion): fail empty bodies without retry
seonghobae Aug 25, 2026
761fdfe
fix(adr): allocate Naruon decision 0203
seonghobae Aug 25, 2026
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
16 changes: 16 additions & 0 deletions CHANGELOG.d/naruon-calendar-projection-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Naruon calendar projection contract

## Added

- Add a strict, bounded v1 consumer contract for calendar occurrences already authorized and policy-filtered by Naruon, including occurrence identity, provider revision, timezone/all-day semantics, disclosure level, and observed provenance.
- Export the projection parser, immutable result types, media type, schema version, and read client through the public LineageWeave package surface.
- Add a reusable bounded JSON response read so oversized pages are rejected before allocation and parsing.

## Changed

- Clarify that LineageWeave owns post-grounded commitments and issue/todo records, while Naruon owns provider CalDAV synchronization, revisions, writeback, retry, and reconciliation.
- Replace the misleading CalDAV label on the earlier custom JSON `/events` feed with an explicit pseudo-CalDAV correction.

## Security

- Reject unsafe base URLs, whitespace/control-bearing service tokens, unbounded response bodies, invalid numeric controls, oversized pages/windows, naive timestamps, duplicate occurrences, unknown fields/vocabularies, and URL-shaped opaque references.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ All notable changes to this project are documented here. Format follows
- `make smoke` and `make seed` now run through the locked project `uv`
environment, so local OIDC and synthetic-data workflows resolve the same
pinned dependencies as CI.
- Provider response parsing now rejects malformed chat envelopes without
exposing provider response bodies, exception text, or secrets through
buyer-facing APIs and persisted ingestion failure details. RankWeave, OIDC,
TEPP, structured VISION, summaries, chat, and extraction channels now retain
stable next-action-safe failure messages.
- Ontology neighborhoods now enforce request bounds before database access,
apply node-level ABAC, omit unlabeled endpoints, preserve catalog-owned node
metadata, and keep typed endpoint IDs unambiguous. Workspace CSV and JSON-LD
Expand Down
4 changes: 2 additions & 2 deletions backend/app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def _jwks(settings: Settings, *, force_refresh: bool = False) -> dict:
except (HttpClientError, OSError, ValueError) as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
f"could not fetch OIDC JWKS for {settings.oidc_issuer}: {exc}",
"could not fetch OIDC JWKS: identity provider unavailable",
) from exc
_jwks_cache[cache_key] = cached
return cached
Expand Down Expand Up @@ -134,7 +134,7 @@ def _decode_access_token(token: str, settings: Settings) -> dict:
except HTTPException:
raise
except jwt.PyJWTError as exc:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"invalid token: {exc}") from exc
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid access token") from exc
subject = claims.get("sub")
if not isinstance(subject, str) or not subject.strip():
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "access token has no subject")
Expand Down
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2763,7 +2763,7 @@ async def chat_about_post(
try:
with use_llm_metadata(post_metadata):
answer = await asyncio.to_thread(client.answer, question, sources)
except (HttpClientError, KeyError, OSError, ValueError) as exc:
except (HttpClientError, KeyError, OSError, TypeError, ValueError) as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Post chat is unavailable: contextual-orchestrator returned no complete evidence object",
Comment thread
seonghobae marked this conversation as resolved.
Expand Down
19 changes: 16 additions & 3 deletions backend/app/post_content_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
_RECOVERY_INTERVAL_SECONDS = 30.0
_INCOMPLETE_FAILURE_CODE = "post_content_ingestion_incomplete"
_ATTEMPT_LIMIT_FAILURE_CODE = "post_content_ingestion_attempt_limit"
_SOURCE_BODY_MISSING_FAILURE_CODE = "post_content_source_body_missing"
_UNEXPECTED_FAILURE_DETAIL = "post-content provider operation failed; retry the ingestion job"


Expand Down Expand Up @@ -229,10 +230,22 @@ async def process_post_content_job(
if row is None:
return
attempt_count = int(row["job_attempt_count"]) + 1
raw_body = row["post_body"]
if not isinstance(raw_body, str) or not raw_body.strip():
_logger.warning(
"post content ingestion skipped: source post has no body",
extra={"post_id": post_id},
)
await _finish_job(
pool,
post_id,
FAILED,
failure_code=_SOURCE_BODY_MISSING_FAILURE_CODE,
detail_text="source post has no body",
expected_attempt_count=attempt_count,
)
Comment thread
seonghobae marked this conversation as resolved.
return
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
try:
raw_body = row["post_body"]
if not isinstance(raw_body, str) or not raw_body.strip():
raise ValueError("source post has no body")
metadata = build_post_llm_metadata(post_id, row)
embedding_client = embedding_factory()
structure_client = structure_factory()
Expand Down
23 changes: 23 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3547,6 +3547,29 @@ def answer(self, question: str, sources) -> ChatAnswer:
assert "What happened here that no seed already answers?" in events[0]["summary"]


def test_post_chat_malformed_provider_reply_is_unavailable(
client, demo_analyst_token, seeded_db, monkeypatch
) -> None:
"""A malformed provider envelope must not escape as an HTTP 500."""

class _MalformedChatClient:
available = True

def answer(self, question: str, sources) -> None:
del question, sources
raise TypeError("provider message content is not a string")

monkeypatch.setattr("backend.app.main._post_chat_client", lambda: _MalformedChatClient())
response = client.post(
f"/api/posts/{seeded_db['own_private_post_id']}/chat",
json={"question": "What happened here?"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)

assert response.status_code == 503
assert "no complete evidence object" in response.json()["detail"]


def test_live_chat_provider_error_does_not_leak_raw_error(
client, demo_analyst_token, seeded_db, monkeypatch
) -> None:
Expand Down
42 changes: 41 additions & 1 deletion backend/tests/test_auth_jwks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import pytest
from fastapi import HTTPException

import backend.app.auth as auth
from backend.app import auth
from lineageweave.http_client import HttpClientError


def _segment(value: dict) -> str:
Expand Down Expand Up @@ -173,3 +174,42 @@ def test_decode_rejects_missing_subject(monkeypatch: pytest.MonkeyPatch) -> None
with pytest.raises(HTTPException) as error:
auth._decode_access_token("token", settings)
assert error.value.status_code == 401


def test_decode_hides_raw_jwt_provider_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(auth, "_signing_key", lambda settings, token: "signing-key")

def fail_decode(*args: object, **kwargs: object) -> dict:
raise auth.jwt.InvalidTokenError("provider secret")

monkeypatch.setattr(auth.jwt, "decode", fail_decode)
settings = SimpleNamespace(
oidc_issuer="https://id.example",
oidc_audience="lineageweave-api",
oidc_clock_skew_seconds=5,
)

with pytest.raises(HTTPException) as error:
auth._decode_access_token("token", settings)
assert error.value.detail == "invalid access token"
assert "provider secret" not in str(error.value.detail)


def test_jwks_hides_raw_identity_provider_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
auth,
"get_json",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
HttpClientError("identity provider secret")
),
)
settings = SimpleNamespace(
oidc_issuer="https://id.example",
oidc_discovery_uri="https://id.example/.well-known/openid-configuration",
oidc_jwks_uri_override="",
)

with pytest.raises(HTTPException) as error:
auth._jwks(settings)
assert error.value.detail == "could not fetch OIDC JWKS: identity provider unavailable"
assert "identity provider secret" not in str(error.value.detail)
7 changes: 7 additions & 0 deletions docs/adr/0030-external-llm-gateway-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ runtime, with masking enabled; the repository contains no provider secret.
The contextual-orchestrator service remains the only LLM boundary. LineageWeave
does not call the provider gateway directly and does not create a fallback
local score, summary, extraction, or answer when the gateway is unavailable.
Provider response envelopes and exceptions are also a trust boundary: clients
extract only the expected content and expose stable, next-action-safe error
messages. Raw provider response bodies, exception text, prompts, and secrets
must never be returned through a buyer-facing API or persisted failure detail.

## Consequences

- Provider changes are deployment configuration, not source changes.
- A missing or invalid gateway credential fails at the orchestrator boundary;
it must not be replaced by a fabricated channel result.
- A provider failure is logged with internal correlation context where
operational logging permits, but the buyer receives a stable unavailable
message that tells them to retry or restore the provider configuration.
- `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS` must explicitly allow the
hostname selected by `LLM_GATEWAY_API_URL`; wildcard allowlists are forbidden.
- Local Compose development permits only the explicitly enumerated
Expand Down
58 changes: 37 additions & 21 deletions docs/adr/0038-calendar-source-contract.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
# ADR 0038: Separate CalDAV events from internal commitments
# ADR 0038: Separate external calendar events from internal commitments

- Status: Accepted
- Status: Superseded in part by ADR 0203
- Date: 2026-08-18

## Context

The buyer Calendar destination needs both external calendar events and
The buyer Calendar destination needs both external calendar observations and
actionable records derived from LineageWeave posts. They have different
ownership and evidence boundaries. PR #251 defines CalDAV as an independent
consumer port, while the current application already stores authorized
commitments and issue tickets.
ownership and evidence boundaries. The application already stores authorized
commitments and issue tickets, while the first external adapter read a custom
JSON `GET {CALDAV_BASE_URL}/events` feed.

## Decision
That feed was not a CalDAV client or server contract. It did not implement RFC
4791 WebDAV discovery/REPORT, RFC 5545 recurrence and timezone semantics, RFC
6578 synchronization, provider revisions, or provider authorization. Product
and code language must not represent it as shipped CalDAV interoperability.

`GET /api/calendar` returns two independent collections:
## Original decision retained

- `events`: events read from `CALDAV_BASE_URL/events` through
`lineageweave.caldav_client`; malformed external rows are ignored.
The Buyer Calendar returns two independent collections:

- `events`: externally observed calendar occurrences; and
- `commitments`: the existing authorized internal commitment projection,
filtered by the requesting account's `post_read` RBAC and post ABAC rules.

When CalDAV is unset or temporarily unavailable, `events` is empty and the
response includes a next action in `calendar_sources`; the internal
commitments remain available. The backend never invents an external event.
When the external calendar channel is unset or temporarily unavailable,
`events` is empty and the internal commitments remain available. The backend
never invents an external event.

LineageWeave does not add a second calendar database, CalDAV server, provider
credential store, or writeback engine.

## Superseding decision

ADR 0203 replaces the custom `/events` transport and CalDAV naming with a
versioned, read-only Naruon calendar projection contract. Naruon is the authority
for customer-owned provider access, source registry, synchronization, provider
revisions, writeback, retries, and reconciliation. LineageWeave consumes only
bounded, already-authorized `observed` occurrence projections.

This checkpoint does not add a second calendar database. A persistent event
store and sync history may be added when offline access, change tracking, or
CalDAV write-back becomes a product requirement.
The original separation between `events` and `commitments` remains mandatory.
An external event is not converted to an internal issue or commitment without a
separate source-grounded LineageWeave decision and evidence trail.

## Consequences

- The Calendar screen is useful with the existing synthetic commitment data,
even without an external calendar server.
- External events cannot be mistaken for post-grounded commitments.
- CalDAV transport failures do not turn the entire buyer surface into a
fail-closed blank screen.
- The Calendar remains useful with authorized commitment data when Naruon is
unavailable.
- External observations cannot be mistaken for post-grounded commitments.
- Product documentation no longer represents a custom JSON feed as CalDAV.
- Runtime activation waits for Naruon's matching read endpoint and service
audience; absence continues to fail closed rather than fabricate events.
Loading
Loading