Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 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
4eab338
test: specify ontology Pages publication contract
seonghobae Aug 21, 2026
78e97e2
feat: build deterministic ontology Pages artifact
seonghobae Aug 21, 2026
b6e6bd8
ci: publish ontology through GitHub Pages
seonghobae Aug 21, 2026
bd11574
docs: record public ontology publication boundary
seonghobae Aug 21, 2026
2355baa
docs: record ontology publication pipeline
seonghobae Aug 21, 2026
709aba5
docs: track public ontology publication gap
seonghobae Aug 21, 2026
a9eced0
fix(test): validate real ontology content and HTML escaping
seonghobae Aug 21, 2026
c4b4643
test: cover empty ontology term categories
seonghobae Aug 21, 2026
d9397e7
fix: protect ontology Pages deployments
seonghobae Aug 21, 2026
536249d
security: harden ontology publication boundary
seonghobae Aug 21, 2026
774a3d9
test: cover ontology publication safety boundary
seonghobae Aug 21, 2026
c99da18
ci: gate Pages publication through the hardened wrapper
seonghobae Aug 21, 2026
96f8c67
fix: preserve valid multi-type ontology terms
seonghobae Aug 21, 2026
a7deec6
test: accept multi-type terms after renderer deduplication
seonghobae Aug 21, 2026
034fa29
docs: record ontology publication safety and namespace follow-up
seonghobae Aug 21, 2026
f85dc42
fix: preserve login return URL and guard admin token
seonghobae Aug 21, 2026
d02aa19
Merge remote-tracking branch 'refs/remotes/origin/lineage-pages-curre…
seonghobae Aug 21, 2026
a081321
docs: track ontology namespace and publication safety gaps
seonghobae Aug 21, 2026
5407c52
docs: file ontology pages for next release
seonghobae Aug 21, 2026
2d2caf7
docs: record hardened ontology publication safeguards
seonghobae Aug 21, 2026
71dc7dc
Merge remote-tracking branch 'refs/remotes/origin/lineage-pages-curre…
seonghobae Aug 21, 2026
f61c98f
docs: note hardened ontology publication wrapper
seonghobae Aug 21, 2026
4c3e43f
Merge remote-tracking branch 'refs/remotes/origin/ci/publish-ontology…
seonghobae Aug 21, 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
135 changes: 135 additions & 0 deletions .github/workflows/ontology-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Ontology Pages

on:
pull_request:
branches: [main]
paths:
- "docs/ontology/**"
- "scripts/build_ontology_site.py"
- "scripts/publish_ontology_site.py"
- "tests/test_ontology.py"
- "tests/test_ontology_site.py"
- "tests/test_publish_ontology_site.py"
- ".github/workflows/ontology-pages.yml"
- "pyproject.toml"
- "uv.lock"
push:
branches: [main]
paths:
- "docs/ontology/**"
- "scripts/build_ontology_site.py"
- "scripts/publish_ontology_site.py"
- "tests/test_ontology.py"
- "tests/test_ontology_site.py"
- "tests/test_publish_ontology_site.py"
- ".github/workflows/ontology-pages.yml"
- "pyproject.toml"
- "uv.lock"
workflow_dispatch:

permissions:
contents: read

jobs:
validate:
name: Validate ontology publication
if: github.event_name == 'pull_request'
concurrency:
group: ontology-pages-validation-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6
with:
python-version: "3.12"

- name: Set up locked Python dependency manager
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.28"
enable-cache: false

- name: Install committed dependencies
run: uv sync --frozen --extra dev

- name: Verify ontology and publication contracts
run: |
uv run --frozen python -m pytest -q tests/test_ontology.py
uv run --frozen python -m coverage run --branch \
-m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py
uv run --frozen python -m coverage report \
--include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \
--fail-under=100

- name: Build static ontology site
run: uv run --frozen python scripts/publish_ontology_site.py --output-dir _site

- name: Compile owned Python surface
run: >-
uv run --frozen python -m compileall -q
scripts/build_ontology_site.py scripts/publish_ontology_site.py
tests/test_ontology_site.py tests/test_publish_ontology_site.py

publish:
name: Publish ontology to GitHub Pages
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
concurrency:
group: ontology-pages-publication
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6
with:
python-version: "3.12"

- name: Set up locked Python dependency manager
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.28"
enable-cache: false

- name: Install committed dependencies
run: uv sync --frozen --extra dev

- name: Verify exact protected source before publication
run: |
uv run --frozen python -m pytest -q tests/test_ontology.py
uv run --frozen python -m coverage run --branch \
-m pytest -q tests/test_ontology_site.py tests/test_publish_ontology_site.py
uv run --frozen python -m coverage report \
--include=scripts/build_ontology_site.py,scripts/publish_ontology_site.py \
--fail-under=100

- name: Build deterministic publication artifact
run: uv run --frozen python scripts/publish_ontology_site.py --output-dir _site

- name: Configure GitHub Pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: _site

- name: Deploy GitHub Pages artifact
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
12 changes: 12 additions & 0 deletions CHANGELOG.d/2.22.0-ontology-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Added

- Added a deterministic GitHub Pages publication pipeline for the public
ontology documentation URL, with fragment-addressable terms and Turtle,
JSON-LD, N-Triples, PROV-O profile, and source-digest artifacts.
- Added semantic round-trip, byte-determinism, fail-closed source, CLI, and
100% statement/branch coverage tests for the ontology site renderer.
- Added a fail-closed publication boundary that prevents duplicate public
fragments, unsafe linked IRI schemes, symlink or source-overlapping outputs,
and deletion of output directories not marked as generated.
- Restricted Pages deployment to `main`, preserved non-cancelling publication
concurrency, and kept all third-party Actions pinned by full commit SHA.
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 @@ -11,6 +11,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.

## [2.12.6] - 2026-08-20

Expand Down
2 changes: 1 addition & 1 deletion backend/app/analysis_run_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def transport(payload: dict[str, Any]) -> dict[str, Any]:
headers = {"authorization": f"Bearer {api_key}"} if api_key.strip() else {}
return post_json(url, payload, headers=headers, timeout=30.0)
except (HttpClientError, OSError, ValueError, TypeError) as exc:
raise TeppNotAvailable(str(exc)) from exc
raise TeppNotAvailable("TEPP transport unavailable") from exc

return TeppClient(transport=transport)

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 @@ -2650,7 +2650,7 @@ async def ask_agent(
except (HttpClientError, KeyError, OSError, ValueError) as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
f"Ask Agent is unavailable: {exc}",
"Ask Agent is unavailable: contextual-orchestrator returned no complete evidence object",
) from exc
cited_ids = list(answer.cited_post_ids)
return {
Expand Down
3 changes: 2 additions & 1 deletion 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"
_UNEXPECTED_FAILURE_DETAIL = "post-content provider operation failed; retry the ingestion job"


async def _stream_tail(client: redis.Redis) -> str:
Expand Down Expand Up @@ -267,7 +268,7 @@ async def process_post_content_job(
pool,
post_id,
failure_code="post_content_ingestion_failed",
detail_text=str(exc)[:1000],
detail_text=_UNEXPECTED_FAILURE_DETAIL,
expected_attempt_count=attempt_count,
)
return
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 0123
- 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 0123 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