diff --git a/.env.example b/.env.example index 7282c5a2e..7af383ea1 100644 --- a/.env.example +++ b/.env.example @@ -52,10 +52,8 @@ LLM_GATEWAY_API_KEY= LLM_GATEWAY_EMBEDDING_MODEL= LLM_API_GATEWAY= LLM_API_KEY= -CALDAV_BASE_URL= # Optional Naruon calendar projection consume (ADR 0203 step 2 / #336). # Empty keeps observed events fail-closed. Never put an end-user bearer here. -# CALDAV_BASE_URL is not a fallback for this audience. NARUON_CALENDAR_BASE_URL= NARUON_CALENDAR_SERVICE_TOKEN= RANKWEAVE_DISABLED= diff --git a/CHANGELOG.d/remove-obsolete-caldav-boundary.md b/CHANGELOG.d/remove-obsolete-caldav-boundary.md new file mode 100644 index 000000000..6f3a09487 --- /dev/null +++ b/CHANGELOG.d/remove-obsolete-caldav-boundary.md @@ -0,0 +1,3 @@ +## Changed + +- Removed the superseded direct pseudo-CalDAV adapter and `CALDAV_BASE_URL` runtime/deployment configuration. External calendar observations remain available only through the strict audience-scoped Naruon projection consumer defined by ADR 0203; historical ADR text describing the retired `/events` experiment remains as migration evidence. diff --git a/backend/app/config.py b/backend/app/config.py index 0fea9a591..aa51f0225 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -60,7 +60,6 @@ class Settings: searxng_base_url: str tepp_transport_url: str tepp_api_key: str - caldav_base_url: str naruon_calendar_base_url: str naruon_calendar_service_token: str rankweave_disabled: bool @@ -207,7 +206,6 @@ def load_settings() -> Settings: searxng_base_url=os.environ.get("SEARXNG_BASE_URL", ""), tepp_transport_url=os.environ.get("TEPP_TRANSPORT_URL", ""), tepp_api_key=os.environ.get("TEPP_API_KEY", "").strip(), - caldav_base_url=os.environ.get("CALDAV_BASE_URL", "").strip(), naruon_calendar_base_url=os.environ.get("NARUON_CALENDAR_BASE_URL", "").strip(), naruon_calendar_service_token=os.environ.get( "NARUON_CALENDAR_SERVICE_TOKEN", "" diff --git a/backend/app/main.py b/backend/app/main.py index 122165990..7aab6deaa 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -197,10 +197,6 @@ ContextualOrchestratorAdjudicationClient, NullAdjudicationClient, ) -from lineageweave.caldav_client import ( - CALDAV_UNAVAILABLE_NEXT_ACTION, - build_caldav_client, -) from lineageweave.commitment_extraction import ( ContextualOrchestratorCommitmentExtractionClient, NullCommitmentExtractionClient, diff --git a/backend/tests/test_config.py b/backend/tests/test_config.py index f37d89f32..f1d2fd49a 100644 --- a/backend/tests/test_config.py +++ b/backend/tests/test_config.py @@ -106,21 +106,21 @@ def test_local_keycloak_discovery_uses_backend_reachable_base_url(monkeypatch) - assert settings.keyverse_claim_binding_required is False -def test_naruon_calendar_audience_defaults_empty_and_is_not_caldav(monkeypatch) -> None: - """Missing Naruon settings keep the observed-event channel dropped.""" +def test_naruon_calendar_audience_defaults_empty_and_ignores_legacy_caldav_env(monkeypatch) -> None: + """Only the audience-scoped Naruon consumer can configure calendar observations.""" monkeypatch.delenv("NARUON_CALENDAR_BASE_URL", raising=False) monkeypatch.delenv("NARUON_CALENDAR_SERVICE_TOKEN", raising=False) monkeypatch.setenv("CALDAV_BASE_URL", "https://calendar.example/caldav/") settings = load_settings() assert settings.naruon_calendar_base_url == "" assert settings.naruon_calendar_service_token == "" - assert settings.caldav_base_url == "https://calendar.example/caldav/" + assert not hasattr(settings, "caldav_base_url") monkeypatch.setenv("NARUON_CALENDAR_BASE_URL", "https://naruon.example/projection") monkeypatch.setenv("NARUON_CALENDAR_SERVICE_TOKEN", "service-secret") wired = load_settings() assert wired.naruon_calendar_base_url == "https://naruon.example/projection" assert wired.naruon_calendar_service_token == "service-secret" - assert wired.naruon_calendar_service_token != wired.caldav_base_url + assert not hasattr(wired, "caldav_base_url") def test_rankweave_disabled_defaults_off(monkeypatch) -> None: diff --git a/docker-compose.yml b/docker-compose.yml index d0a2422aa..43cbbcf15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -176,7 +176,6 @@ services: SEARXNG_BASE_URL: http://searxng:8080 TEPP_TRANSPORT_URL: ${TEPP_TRANSPORT_URL:-} TEPP_API_KEY: ${TEPP_API_KEY:-} - CALDAV_BASE_URL: ${CALDAV_BASE_URL:-} NARUON_CALENDAR_BASE_URL: ${NARUON_CALENDAR_BASE_URL:-} NARUON_CALENDAR_SERVICE_TOKEN: ${NARUON_CALENDAR_SERVICE_TOKEN:-} RANKWEAVE_DISABLED: ${RANKWEAVE_DISABLED:-} diff --git a/lineageweave/caldav_client.py b/lineageweave/caldav_client.py deleted file mode 100644 index 3653cf791..000000000 --- a/lineageweave/caldav_client.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Small independent CalDAV event-consumption port for the organization calendar.""" - -from __future__ import annotations - -from dataclasses import dataclass -from urllib.parse import urlparse - -from lineageweave.http_client import get_json - -CALDAV_UNAVAILABLE_NEXT_ACTION = ( - "Configure CALDAV_BASE_URL to connect the independent CalDAV event source." -) - - -@dataclass(frozen=True) -class CalDavEvent: - """One calendar event read from the configured CalDAV source.""" - - event_id: str - summary: str - starts_at: str - - -class NullCalDavClient: - """Fail-closed CalDAV client used when CALDAV_BASE_URL is unset; never fabricates events.""" - - available = False - - def list_events(self) -> list[CalDavEvent]: - """Always return no events; there is no CalDAV source to query.""" - return [] - - -class HttpCalDavClient: - """CalDAV client backed by a real HTTP events endpoint.""" - - available = True - - def __init__(self, base_url: str) -> None: - parsed = urlparse(base_url) - if parsed.scheme not in {"http", "https"} or not parsed.hostname: - raise ValueError("CALDAV_BASE_URL must be an http(s) URL with a hostname") - self._events_url = f"{base_url.rstrip('/')}/events" - - def list_events(self) -> list[CalDavEvent]: - """Fetch and parse events from the configured CalDAV endpoint.""" - payload = get_json( - self._events_url, timeout=10, service_peer_name="caldav" - ) - rows = payload.get("events") - if not isinstance(rows, list): - return [] - events: list[CalDavEvent] = [] - for row in rows: - if not isinstance(row, dict): - continue - values = (row.get("event_id"), row.get("summary"), row.get("starts_at")) - if not all(isinstance(value, str) and value.strip() for value in values): - continue - events.append(CalDavEvent(*(value.strip() for value in values))) - return events - - -def build_caldav_client(base_url: str) -> NullCalDavClient | HttpCalDavClient: - """Return the real source only when explicitly configured.""" - normalized = base_url.strip() - return HttpCalDavClient(normalized) if normalized else NullCalDavClient() diff --git a/tests/test_caldav_client.py b/tests/test_caldav_client.py deleted file mode 100644 index f6453231d..000000000 --- a/tests/test_caldav_client.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import pytest - -from lineageweave.caldav_client import ( - CalDavEvent, - HttpCalDavClient, - NullCalDavClient, - build_caldav_client, -) - - -def test_missing_base_url_drops_only_the_optional_caldav_channel() -> None: - client = build_caldav_client("") - - assert isinstance(client, NullCalDavClient) - assert not client.available - assert client.list_events() == [] - - -def test_http_client_reads_valid_events_and_ignores_malformed_rows(monkeypatch) -> None: - received = {} - - def fake_get_json(url: str, *, timeout: float, **kwargs) -> dict: - received.update(url=url, timeout=timeout, **kwargs) - return { - "events": [ - { - "event_id": "event-1", - "summary": "Review", - "starts_at": "2026-08-19T09:00:00Z", - }, - { - "event_id": "event-2", - "summary": "", - "starts_at": "2026-08-19T10:00:00Z", - }, - "not-an-event", - ] - } - - monkeypatch.setattr("lineageweave.caldav_client.get_json", fake_get_json) - client = build_caldav_client("https://calendar.example/caldav/") - - assert isinstance(client, HttpCalDavClient) - assert client.list_events() == [ - CalDavEvent("event-1", "Review", "2026-08-19T09:00:00Z") - ] - assert received == { - "url": "https://calendar.example/caldav/events", - "timeout": 10, - "service_peer_name": "caldav", - } - - -def test_invalid_caldav_url_is_rejected() -> None: - with pytest.raises(ValueError, match="CALDAV_BASE_URL"): - build_caldav_client("file:///tmp/events") diff --git a/tests/test_calendar_authority_fitness.py b/tests/test_calendar_authority_fitness.py new file mode 100644 index 000000000..d0b9da911 --- /dev/null +++ b/tests/test_calendar_authority_fitness.py @@ -0,0 +1,29 @@ +"""Architecture fitness for external calendar authority boundaries.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_obsolete_direct_caldav_adapter_and_configuration_are_absent() -> None: + """LineageWeave must not retain a direct pseudo-CalDAV provider boundary.""" + assert not (ROOT / "lineageweave" / "caldav_client.py").exists() + assert not (ROOT / "tests" / "test_caldav_client.py").exists() + + config_source = (ROOT / "backend" / "app" / "config.py").read_text(encoding="utf-8") + main_source = (ROOT / "backend" / "app" / "main.py").read_text(encoding="utf-8") + env_example = (ROOT / ".env.example").read_text(encoding="utf-8") + compose_source = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") + + assert "caldav_base_url" not in config_source + assert "CALDAV_BASE_URL" not in config_source + assert "lineageweave.caldav_client" not in main_source + assert "CALDAV_UNAVAILABLE_NEXT_ACTION" not in main_source + assert "build_caldav_client" not in main_source + assert "\nCALDAV_BASE_URL=" not in env_example + assert "CALDAV_BASE_URL:" not in compose_source + + # The versioned consumer/ACL remains explicit while the provider authority stays external. + assert "NARUON_CALENDAR_BASE_URL" in config_source + assert "NARUON_CALENDAR_SERVICE_TOKEN" in config_source