fix: clear the two Starlette deprecation warnings - #218
Conversation
Starlette 1.3.1 deprecated `HTTP_413_REQUEST_ENTITY_TOO_LARGE` in favour of `HTTP_413_CONTENT_TOO_LARGE`. Both resolve to 413, so the swap at the single call site in the project-import size guard is warning-only with no behaviour change. `starlette.testclient` now prefers `httpx2` and warns when it falls back to `httpx`. Add `httpx2` to the dev group so TestClient picks it up. `httpx` stays a runtime dependency — auth, github_service, user_service, the OpenAI embedding provider and sitemap_notifier all use it directly, and several test modules mock `httpx.Response` / `httpx.HTTPStatusError` against that runtime code. The test suite now runs with zero deprecation warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe import endpoint now uses the current HTTP 413 status constant. The development dependency group now includes ChangesImport status response
Development dependency
Estimated code review effort: 1 (Trivial) | ~3 minutes Merge Risk: ⚪ Minimal · up to This PR updates a deprecated status-code name and adds the test dependency needed to remove deprecation warnings without changing runtime behavior; no actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
* chore(deps): bump astral-sh/setup-uv in the github-actions group
Bumps the github-actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv).
Updates `astral-sh/setup-uv` from 8.3.0 to 8.3.2
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/d31148d669074a8d0a63714ba94f3201e7020bc3...11f9893b081a58869d3b5fccaea48c9e9e46f990)
---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
dependency-version: 8.3.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump torch from 2.12.1 to 2.13.0
Bumps [torch](https://github.com/pytorch/pytorch) from 2.12.1 to 2.13.0.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.12.1...v2.13.0)
---
updated-dependencies:
- dependency-name: torch
dependency-version: 2.13.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump transformers from 5.3.0 to 5.5.0
Bumps [transformers](https://github.com/huggingface/transformers) from 5.3.0 to 5.5.0.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.3.0...v5.5.0)
---
updated-dependencies:
- dependency-name: transformers
dependency-version: 5.5.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): update uvicorn[standard] requirement
Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn) to permit the latest version.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.49.0...0.51.0)
---
updated-dependencies:
- dependency-name: uvicorn[standard]
dependency-version: 0.50.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump gitpython from 3.1.50 to 3.1.54
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.50 to 3.1.54.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.50...3.1.54)
---
updated-dependencies:
- dependency-name: gitpython
dependency-version: 3.1.54
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [docker/login-action](https://github.com/docker/login-action).
Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)
Updates `docker/login-action` from 4.4.0 to 4.5.1
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: 7.0.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
- dependency-name: docker/login-action
dependency-version: 4.5.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump astral-sh/setup-uv from 8.3.2 to 9.0.0
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/11f9893b081a58869d3b5fccaea48c9e9e46f990...c771a70e6277c0a99b617c7a806ffedaca235ff9)
---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
dependency-version: 9.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump docker/login-action in the github-actions group
Bumps the github-actions group with 1 update: [docker/login-action](https://github.com/docker/login-action).
Updates `docker/login-action` from 4.5.1 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7...dbcb813823bdd20940b903addbd779551569679f)
---
updated-dependencies:
- dependency-name: docker/login-action
dependency-version: 4.6.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml](https://github.com/google/osv-scanner-action) and [google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml](https://github.com/google/osv-scanner-action).
Updates `google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml` from 2.3.8 to 2.5.0
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/9a498708959aeaef5ef730655706c5a1df1edbc2...8deb546fdb875b9996d27d4950be7312dac076a1)
Updates `google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml` from 2.3.8 to 2.5.0
- [Release notes](https://github.com/google/osv-scanner-action/releases)
- [Commits](https://github.com/google/osv-scanner-action/compare/9a498708959aeaef5ef730655706c5a1df1edbc2...8deb546fdb875b9996d27d4950be7312dac076a1)
---
updated-dependencies:
- dependency-name: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml
dependency-version: 2.5.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml
dependency-version: 2.5.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump astral-sh/setup-uv from 9.0.0 to 10.0.0
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 9.0.0 to 10.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/c771a70e6277c0a99b617c7a806ffedaca235ff9...ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d)
---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
dependency-version: 10.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump setuptools from 81.0.0 to 83.0.0 (#193)
Bumps [setuptools](https://github.com/pypa/setuptools) from 81.0.0 to 83.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v81.0.0...v83.0.0)
---
updated-dependencies:
- dependency-name: setuptools
dependency-version: 83.0.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John R. D'Orazio <priest@johnromanodorazio.com>
* chore(deps): bump cryptography from 48.0.1 to 50.0.0 (#200)
Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.1 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.1...50.0.0)
---
updated-dependencies:
- dependency-name: cryptography
dependency-version: 50.0.0
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John R. D'Orazio <priest@johnromanodorazio.com>
* chore(deps): bump the python-dependencies group across 1 directory with 11 updates
Bumps the python-dependencies group with 11 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [fastapi](https://github.com/fastapi/fastapi) | `0.138.1` | `0.141.1` |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) | `2.14.2` | `2.15.0` |
| [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) | `2.0.51` | `2.0.52` |
| [alembic](https://github.com/sqlalchemy/alembic) | `1.18.5` | `1.19.1` |
| [gitpython](https://github.com/gitpython-developers/GitPython) | `3.1.54` | `3.1.59` |
| [pygit2](https://github.com/libgit2/pygit2) | `1.19.3` | `1.20.0` |
| [sentence-transformers](https://github.com/huggingface/sentence-transformers) | `5.6.0` | `5.7.0` |
| [pgvector](https://github.com/pgvector/pgvector-python) | `0.4.2` | `0.5.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.20` | `0.16.2` |
| [mypy](https://github.com/python/mypy) | `2.1.0` | `2.3.0` |
| [pre-commit](https://github.com/pre-commit/pre-commit) | `4.6.0` | `4.6.2` |
Updates `fastapi` from 0.138.1 to 0.141.1
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.138.1...0.141.1)
Updates `pydantic-settings` from 2.14.2 to 2.15.0
- [Release notes](https://github.com/pydantic/pydantic-settings/releases)
- [Commits](https://github.com/pydantic/pydantic-settings/compare/v2.14.2...v2.15.0)
Updates `sqlalchemy` from 2.0.51 to 2.0.52
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)
Updates `alembic` from 1.18.5 to 1.19.1
- [Release notes](https://github.com/sqlalchemy/alembic/releases)
- [Changelog](https://github.com/sqlalchemy/alembic/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/alembic/commits)
Updates `gitpython` from 3.1.54 to 3.1.59
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.54...3.1.59)
Updates `pygit2` from 1.19.3 to 1.20.0
- [Release notes](https://github.com/libgit2/pygit2/releases)
- [Changelog](https://github.com/libgit2/pygit2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/libgit2/pygit2/compare/v1.19.3...v1.20.0)
Updates `sentence-transformers` from 5.6.0 to 5.7.0
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v5.6.0...v5.7.0)
Updates `pgvector` from 0.4.2 to 0.5.0
- [Changelog](https://github.com/pgvector/pgvector-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgvector/pgvector-python/compare/v0.4.2...v0.5.0)
Updates `ruff` from 0.15.20 to 0.16.2
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.20...0.16.2)
Updates `mypy` from 2.1.0 to 2.3.0
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v2.1.0...v2.3.0)
Updates `pre-commit` from 4.6.0 to 4.6.2
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.6.0...v4.6.2)
---
updated-dependencies:
- dependency-name: fastapi
dependency-version: 0.141.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: pydantic-settings
dependency-version: 2.15.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: sqlalchemy
dependency-version: 2.0.52
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: python-dependencies
- dependency-name: alembic
dependency-version: 1.19.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: gitpython
dependency-version: 3.1.59
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: python-dependencies
- dependency-name: pygit2
dependency-version: 1.20.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: sentence-transformers
dependency-version: 5.7.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: pgvector
dependency-version: 0.5.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: ruff
dependency-version: 0.16.2
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: mypy
dependency-version: 2.3.0
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: python-dependencies
- dependency-name: pre-commit
dependency-version: 4.6.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: python-dependencies
...
Signed-off-by: dependabot[bot] <support@github.com>
* fix(types): keep pgvector Vector fallback typed as Any (#215)
pgvector 0.5.0 ships type information, so mypy 2.3.0 now resolves
`pgvector.sqlalchemy.Vector` to a real class. That made the existing
conditional import fail three ways: the `# type: ignore` became unused,
and the `Vector = None` fallback tried to assign None to a type.
Declare `Vector: Any` up front and assign in try/except/else, so the
runtime `Vector is None` guards in embedding_service stay valid for type
checkers and remain patchable in tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: clear the two Starlette deprecation warnings (#218)
Starlette 1.3.1 deprecated `HTTP_413_REQUEST_ENTITY_TOO_LARGE` in favour
of `HTTP_413_CONTENT_TOO_LARGE`. Both resolve to 413, so the swap at the
single call site in the project-import size guard is warning-only with no
behaviour change.
`starlette.testclient` now prefers `httpx2` and warns when it falls back
to `httpx`. Add `httpx2` to the dev group so TestClient picks it up.
`httpx` stays a runtime dependency — auth, github_service, user_service,
the OpenAI embedding provider and sitemap_notifier all use it directly,
and several test modules mock `httpx.Response` / `httpx.HTTPStatusError`
against that runtime code.
The test suite now runs with zero deprecation warnings.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(08-01): add AUTH_MODE config and anonymous user for three auth modes
- Add auth_mode field (required/optional/disabled) to Settings class in config.py
- Add ANONYMOUS_USER constant to auth.py (id=anonymous, roles=[viewer])
- Add early returns in get_current_user, get_current_user_optional, get_current_user_with_token for disabled mode
- Optional mode: RequiredUser still 401s (write protection), OptionalUser returns None (browse works)
* test(08-01): add tests for all three auth modes (required, optional, disabled)
- Test ANONYMOUS_USER properties (id, roles, type, not superadmin)
- Test disabled mode: all three auth functions return ANONYMOUS_USER
- Test required mode: get_current_user raises 401, get_current_user_optional returns None
- Test optional mode: RequiredUser raises 401 (write protection), OptionalUser returns None (browse works)
* harden(auth): Literal auth_mode + disabled-ignores-credentials test
/ce:review (PR-2) follow-ups:
- config.py: auth_mode str -> Literal[required|optional|disabled] so
pydantic-settings rejects config typos at startup (fail-fast) instead of
silently falling through to required behavior.
- test_auth_disabled.py: add test_disabled_ignores_valid_credentials proving
disabled mode ignores even a valid Bearer token (everyone anonymous viewer).
WS-auth parity (authenticate_ws does not consult auth_mode) is intentionally
NOT changed here — it is new work beyond the extracted phase-08 slice; noted
on the PR for a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(auth): WebSocket auth honors auth_mode (HTTP parity)
authenticate_ws hard-required a token regardless of settings.auth_mode,
so a disabled/optional deployment admitted anonymous callers on its HTTP
API but rejected them at the WebSocket handshake. Resolve caller identity
per auth_mode mirroring core.auth:
- disabled -> ANONYMOUS_USER, no token required
- optional -> absent/invalid token downgrades to anonymous (get_current_user_optional)
- required -> valid token mandatory (unchanged)
Project-access check still gates private projects for anonymous callers.
+7 parity tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* feat(10-01): anonymous token module, extended model, and schemas
- Add ontokit/core/anonymous_token.py with HMAC-signed create/verify functions (24h TTL, anon: prefix to prevent token type confusion with beacon tokens)
- Extend SuggestionSession model with is_anonymous, submitter_name, submitter_email, and client_ip columns
- Add ontokit/schemas/anonymous_suggestion.py with AnonymousSessionCreateResponse, AnonymousSubmitRequest (honeypot field aliased as 'website'), and AnonymousSubmitResponse
* feat(10-01): anonymous suggestion endpoints, service methods, and rate limiting
- Add ontokit/api/routes/anonymous_suggestions.py with create, save, submit, discard, and beacon endpoints
- All endpoints gate on AUTH_MODE != 'required' (return 403 otherwise)
- Rate limit: create endpoint checks for >= 5 anonymous sessions from same IP in last hour, returns 429
- Save/submit/discard authenticate via X-Anonymous-Token header using verify_anonymous_token()
- Submit endpoint silently returns fake success for honeypot-filled bot requests
- Add create_anonymous_session, save_anonymous, submit_anonymous, discard_anonymous methods to SuggestionService
- Register anonymous_suggestions router in ontokit/api/routes/__init__.py under /projects prefix
* feat(10-01): update review summaries to show anonymous submitter info
- Add is_anonymous field to SuggestionSessionSummary schema
- _build_summary: use submitter_name/email (credit info) over user_name/email for anonymous sessions
- _create_pr_for_session: show 'Submitted anonymously' or 'Submitted by {name}' for anonymous sessions
- Existing authenticated session summaries are unchanged (backward compatible)
* feat: add alembic migration for anonymous suggestion fields
Adds is_anonymous, submitter_name, submitter_email, client_ip columns
to suggestion_sessions table. These were added to the model in plan
10-01 but the migration was missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* harden(pr-7): fix dead anonymous beacon + public-project gate + endpoint tests
- BLOCKER-class functional fix: the lineage's anonymous beacon route passed
data.session_id where beacon_save expects a BEACON token — verify_beacon_token
(session_id) always returned None, so the endpoint 401'd on every request
(fails closed; dead code). New SuggestionService.beacon_save_anonymous binds
the route-verified X-Anonymous-Token session to the payload session (403 on
mismatch), re-checks is_anonymous (an anonymous token must never flush an
authenticated session), and shares the branch-locked flush via _beacon_flush.
- Security gate: create_anonymous_session now requires project.is_public —
anonymous users could previously create suggestion branches/PRs against
PRIVATE projects (rate limiting was the only guard).
- Tests (lineage shipped none): 11 new — AUTH_MODE 403 sweep, garbage-token
401s, verified-session forwarding (save + beacon), honeypot silent fake
success without service call, service-level private-project/mismatch/
authenticated-session guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-7): apply /ce:review findings — global IP rate limit, anonymous branch reaper, honeypot de-disclosure
- HIGH: 5-sessions/hour rate limit was scoped per (project_id, IP) — one IP
could mint 5 sessions+branches on EVERY public project per hour. Now global
per-IP (the docstring's original intent).
- HIGH: anonymous git branches were never garbage-collected — sessions with
changes_count==0 matched no sweep at all, and the authed sweep's access
re-check always fails for the anonymous pseudo-user, discarding WITHOUT
branch delete (orphaned-branch leak = unbounded unauthenticated git-storage
abuse). New reap_stale_anonymous_sessions (atomic claim, discard + force
branch delete at 24h = token TTL, includes zero-change sessions), wired
into the worker cron next to the authed sweep, which now excludes
anonymous sessions by design.
- MEDIUM: honeypot semantics were disclosed verbatim in the public OpenAPI
schema (field description, model docstring, route docstring) — now reads
as an ordinary optional website URL; mechanism documented in code comments
only. OpenAPI scan pins no leak.
- MEDIUM (tests): new test_anonymous_token.py — TTL expiry, tamper, malformed,
insecure-secret guard, and BOTH directions of anonymous<->beacon cross-token
rejection (same-secret confusion). Reaper behavioral tests (branch delete +
concurrent-claim skip). Worker test reconciled.
Follow-ups noted on the PR (not blocking): is_public re-check on session
lifecycle, PR title/body sanitization, content max_length, proxy-topology
note for request.client.host, rate-limit TOCTOU.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* test(auth): isolate anonymous token secret
* feat(11-01): DB models, schemas, and Alembic migration for LLM config
- Add ProjectLLMConfig model with provider, encrypted key, model_tier, base_url, budget fields
- Add LLMAuditLog model with token counts, cost_estimate_usd, is_byo_key flag
- Add can_self_merge_structural boolean to ProjectMember model
- Add LLMProviderType enum (13 providers) and LLM Pydantic schemas in schemas/llm.py
- Add Alembic migration u9v0w1x2y3a4 — applies cleanly against live DB
- Export new models from ontokit/models/__init__.py
* feat(11-01): LLM provider registry, crypto helpers, pricing, and SSRF validator
- Port 13-provider registry from folio-enrich with get_provider() factory
- OpenAICompatProvider handles 9 providers via OpenAI SDK base_url param
- AnthropicProvider, GoogleProvider (httpx REST), CohereProvider, GitHubModelsProvider
- All chat() methods return (text, input_tokens, output_tokens) for audit logging
- crypto.py: Fernet encrypt_secret/decrypt_secret using same pattern as embedding_service.py
- pricing.py: LiteLLM pricing fetch with 7-day module-level cache, graceful stale fallback
- ssrf.py: validate_base_url() blocks private IPs, metadata endpoint, non-https for cloud
- Add openai, anthropic, google-generativeai, cohere to pyproject.toml dependencies
* feat(pr-3): LLM config/test-connection/usage routes + shared audit module
Carved from phase-11 commit 9d1d7ca (mixed PR-3/PR-4). Includes the PR-3
routes only — config GET/PUT, test-connection, usage, and public
providers/known-models — and registers them. The PR-4 routes (GET /llm/status
and the can_self_merge_structural member-flags PATCH) are dropped here and ship
with cost-controls (PR-4). audit.py (from PR-4 commit 119e501) is pulled in as
the shared backing for GET /llm/usage; budget/rate_limiter/role_gates stay in PR-4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-3): refuse default secret for API-key encryption + CI lint/type fixes
- crypto.py: _get_fernet() now hard-fails in production (and warns in dev) when
SECRET_KEY is the shipped 'change-me-in-production' default. Because this key
encrypts user/BYO provider API keys, a publicly-known secret would make them
trivially decryptable — equivalent to plaintext. (Addresses the session-4
hardcoded-secret class of finding for the BYOK lineage.)
- registry.py: fix latent UnboundLocalError — the unknown-provider handler
interpolated an unbound 'provider_type'; use 'name'. Also raise ... from None.
- ruff/mypy: StrEnum for LLMProviderType (UP042); dict[str, ...] type args;
provider _client: Any annotations; ssrf addr str(); B904 raise-from. Net-new
files must pass CI's authoritative mypy + ruff.
- tests: real secrets tests (crypto round-trip, response never exposes key,
write-only api_key, production default-secret block, audit metadata-only)
replacing the phase-11 skipped stubs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* test(pr-3): public LLM catalogue endpoint tests (providers/known-models)
Exercises the two no-auth PR-3 routes through the real FastAPI app (route
registration + response_model serialization), asserts no secret material in the
public catalogue, and confirms project-scoped /llm/config is auth-gated. This is
the PR-3 surface verifiable without an authenticated, seeded project.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-3): apply /ce:review findings (secret-key scope, SSRF, usage TZ)
Kieran-python review (no BLOCKER):
- H1: secret-key guard now hard-fails in ANY deployed env (staging + production),
not just production. Staging is shared/reachable and may hold real tenant keys;
a log warning there is not a control. Uses "not settings.is_development".
- H2: SSRF was write-time only (TOCTOU / DNS-rebinding). Re-validate base_url in
test-connection immediately before the outbound call (the only PR-3 outbound
path); harden metadata detection to catch IPv4-mapped IPv6 (::ffff:169.254...)
and AWS IPv6 IMDS (fd00:ec2::254), and unwrap mapped IPv6 in private-IP checks.
Full connect-time IP pinning + redirect disabling + the PR-5 chat path are
noted as follow-ups.
- M1: usage aggregation compared a timestamptz column against a naive UTC
wall-clock (NOW AT TIME ZONE UTC), shifting month/day boundaries under a
non-UTC DB session. Now uses tz-aware date_trunc(field, now, 'UTC') and
now - interval, so totals + budget% are correct regardless of session TimeZone.
- L1: a base_url-only update on an existing local (Ollama) config is no longer
rejected -- effective provider falls back to the stored row.
Tests: staging block (parametrized), SSRF metadata/private-IP detection incl.
IPv6 forms. ruff + mypy clean; full suite 1547 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-3): SSRF connect-time pinning + Fernet MultiFernet rotation
Two queued follow-ups from the PR-3 /ce:review:
SSRF connect-time re-validation (kills the resolve-then-connect TOCTOU):
- ssrf.resolve_and_validate() resolves + validates every address at call time
- SSRFProtectedTransport re-validates the target host on every request,
immediately before the socket opens, so a base_url that passed validation
at config-save can't later DNS-rebind to a private/metadata IP
- secure_async_client() wires that transport and disables redirect following
(a 3xx to a private IP can't bypass the guard)
- wired into every provider that dials a project-controlled base_url:
cohere/google/github_models (raw httpx) + openai_compat (openai SDK
http_client); local providers pass allow_private=True (metadata still blocked)
Fernet key rotation via MultiFernet:
- crypto._get_fernet() now builds a MultiFernet: current SECRET_KEY encrypts,
retired keys in SECRET_KEY_PREVIOUS decrypt only -> zero-downtime rotation
- rotate_secret() re-encrypts stored ciphertext onto the current key
- the shipped insecure default is never trusted as a rotation key
- new config.secret_key_previous (comma-separated)
+13 tests (SSRF resolver/rebinding/redirects; rotation round-trip/drop/migrate).
Full suite 1575 green; ruff + mypy-strict clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* feat(pr-4): cost controls & role gating — budget/rate-limit/role-gate services + status/member-flags routes
Reintroduces the PR-4 pieces deferred from PR-3:
- services/llm/{budget,rate_limiter,role_gates}.py carved from the phase-11
lineage (119e501); __init__.py re-exports restored
- routes/llm.py: GET /projects/{id}/llm/status (member-readable, advisory —
dispatch re-checks in PR-5) + PATCH /projects/{id}/members/{uid}/flags
(owner/admin-only ROLE-03 toggle), hand-inserted from 9d1d7ca so PR-3's
hardening (TOCTOU SSRF re-validation, effective_provider fallback) stays
- MemberFlagsUpdate/Response schemas moved to schemas/llm.py (lineage had
them inline in the routes file with an aliased BaseModel import)
- Activates the inert PR-3 carry-alongs: LLMStatusResponse schema and the
can_self_merge_structural column/migration
Deviations from lineage (hardening folded in):
- budget.py time windows now tz-aware func.date_trunc(..., 'UTC') — mirrors
the PR-3 review fix already applied to audit.py (naive-UTC text() SQL
mis-windows spend against timestamptz)
- rate_limiter.py: UTC day key (was server-local date.today()) so the rate
window aligns with budget/audit windows; EXPIRE now NX+unconditional,
repairing keys left TTL-less by a crash between INCR and EXPIRE (which
permanently rate-limited the user once over cap)
- /llm/status: no-access roles report daily_remaining=0 (lineage reported
null, which the schema documents as 'uncapped')
- member-flags PATCH logs privilege changes (actor/target/value, metadata
only) — it grants structural self-merge and previously left no trace
- mypy strict: BudgetStatus TypedDict + typed role descriptors; dead lineage
imports/_MemberFlagsUpdate class dropped
Net-new tests (lineage had only skip-stubs): rate-limit matrix incl. NX TTL
+ documented fail-open pin; budget matrix incl. BYO-exclusion asserted
against compiled SQL + daily-before-monthly ordering; role-gate matrix incl.
anonymous override + RATE_LIMITS consistency; status/member-flags route
tests (auth boundaries 401/403/404/422, viewer=0, owner=null, exhaustion,
local-provider, no-key-leak). 55 tests; full suite 1617.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-4): apply /ce:review findings (budget-pct scale, daily-remaining invariant, query consolidation + tests)
Completes the /ce:review pass on the cost-control layer (was session-capped
mid-review). Two reviewers (kieran-python + security-sentinel) — SHIP, no
BLOCKER/HIGH.
Service/route hardening (budget/rate_limiter/llm routes):
- budget.py: get_budget_status consolidated 3 queries -> 1 round-trip
(FILTER'd SUMs over month/day/week, LEAST(month_start, week_ago) outer
bound) since it backs the member-polled /llm/status; project_id typed
str -> uuid.UUID; tz-aware date_trunc(..., 'UTC') windows.
- budget.py: budget_consumed_pct now on the 0-100 scale to match
LLMUsageResponse / get_llm_usage (both *100) — the bare-fraction was a
latent 100x mis-render for the first consumer of the snapshot. [MEDIUM]
- rate_limiter.py: typed RateLimitRedis Protocol (drops the object +
type:ignore); _REDIS_INFRA_ERRORS narrows the fail-open catch so a
mis-wired client raises instead of silently disabling metering; EXPIRE
NX repairs TTL-less keys.
- llm.py: explicit `role is not None` superadmin fallback (empty-string
role cannot escalate to admin); unknown provider -> unconfigured, not
500; spend telemetry gated (no-access roles see caps/booleans, not
spend/burn); daily_remaining is now an unconditional static per-role
lookup so a no-access role reports 0 even on an unconfigured project
(the null-reads-as-uncapped invariant must not hinge on config). [LOW]
- role_gates.py: hoisted the per-role descriptor map to a module-level
constant (was rebuilt on every gate call). [LOW]
- models/__init__.py: import ordering (isort).
Tests reconciled + strengthened (no orphaned red):
- Updated the budget-status and status-route test doubles to the single
consolidated-query shape (result.one().monthly/.daily/.week).
- Added a compiled-SQL pin for get_budget_status (BYO exclusion, UTC
date_trunc, LEAST outer bound, 3 FILTER aggregates). [LOW]
- Added a viewer-on-unconfigured-project regression (daily_remaining == 0).
- Full suite 1619 passed; mypy strict 0; ruff 0 on all PR-4 files.
Non-blocking follow-up noted on the PR: alert on the rate-limiter fail-open
WARNING so a sustained Redis outage is visible (the fail-open is bounded —
not yet wired into enforcement here; PR-5 wires it, backstopped by the
non-fail-open DB budget layer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-4): actionable, unified rate-limiter fail-open alert
Both limiter fail-open paths (check_rate_limit, get_remaining_calls) now emit
ONE stable, greppable signal — FAIL_OPEN_EVENT=llm_rate_limiter_fail_open — with
structured extra fields (event/operation/project_id/user_id) and the triggering
error, so ops can alert on a single marker instead of two ad-hoc log lines.
Parity verified: budget layer fails CLOSED (non-fail-open backstop); route-level
fail-open alerting lands with PR-5. +2 tests assert the marker fires on both paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* feat(pr-5): duplicate-check dependency slice (composite scoring + cross-branch search)
Minimal phase-12 chain needed by the suggestion generation pipeline:
- DuplicateRejection model + duplicate_check schemas (from 8226d9e)
- StructuralSimilarityService — folio-python Jaccard, fail-soft to 0.0 (from 9a97712)
- EmbeddingService.semantic_search_all_branches + SemanticSearchResultWithBranch
schema (from e4f8a71), ported to the CAST(:param AS vector) bindparam style this
branch already uses (::vector after a bindparam is silently dropped by
SQLAlchemy text())
- DuplicateCheckService with 40/40/20 exact/semantic/structural scoring (from
a884e27); reviewer-flow hunks (suggestion.py reject-linking fields,
suggestion_service.py) dropped — they belong to the PR-6 review flow
- HNSW index + duplicate_rejections migration (from 11d1d3a); also repairs the
orphaned u9v0w1x2y3a4 down_revision (t8u9v0w1x2y3 was never extracted) to
chain onto 47cc27515626 so alembic history is a single linear head again
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* feat(pr-5): core suggestion generation engine (children/siblings/parents/annotations/edges)
Extracted from the phase-13 lineage (029d309, 220c88c, 4b54f96, c6d3cb7,
3399045, 481284a + final-state tests from 3b1874a/1fa317e):
- schemas/generation.py: request/response contract, Provenance literal,
GeneratedSuggestion with confidence + provenance + embedded validation and
duplicate results
- services/validation_service.py: VALID-01..06 rules, mint_iri,
detect_project_namespace
- services/context_assembler.py: ~2-4K-token ontology context assembly
(quality_filter.py from the same lineage commit dropped — PR-6 reviewer
concern, nothing in the generate path imports it)
- services/llm/prompts/: 5 prompt templates + typed PROMPT_BUILDERS dispatch
- services/suggestion_generation_service.py: context -> LLM -> parse ->
validate -> dedup pipeline; confidence normalization (clamp 0-1, /100 for
0-100 scales); dedup failures fail-soft to verdict=pass
- api/routes/generation.py: POST generate-suggestions + validate-entity;
generate enforces role gate (403), rate limit (429, Redis fail-open),
budget (402) BEFORE any LLM call — closes the PR-4 follow-up where the
rate limiter shipped without enforcement
- tests: generation schemas, entity validation, suggestion generation
(GEN-01..09), context assembler; quality_filter-only tests trimmed
Typing tightened for this branch's strict mypy gate (dict type args,
typed PROMPT_BUILDERS, cast on JSON parse, isinstance guard on confidence).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* feat(pr-5): carry model + prompt-template provenance on each suggestion
Per-suggestion provenance now carries model + prompt identity, not just the
audit log (D-08: metadata only — raw prompt text is never persisted):
- GeneratedSuggestion gains model: str | None and prompt_template: str | None
- SuggestionGenerationService.generate() accepts model_id and stamps
model=model_id, prompt_template=<PROMPT_BUILDERS key> on every suggestion
- generate-suggestions route threads config.model into the pipeline
- tests pin: provenance='llm-proposed', confidence in [0,1], model id, and
prompt_template key on every generated suggestion; model stays None when
no model_id is threaded (user-written paths)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* test(pr-5): generation route enforcement tests (403/429/402/400/404/422/502 + success shape)
Route-level coverage for the enforcement boundary, which the extracted
lineage left thin:
- 401/403 unauthenticated + non-member + viewer role-gate
- 429 rate-limit exceeded — pins that check_rate_limit fires BEFORE any
provider construction or budget check (the PR-4 'shipped but not wired'
follow-up), plus the documented Redis fail-open path
- 402 budget exhausted / daily cap, provider never constructed
- 400 missing LLM config, 422 batch_size bounds, 404 unknown project and
unknown class_iri, 502 provider auth error
- success shape pins provenance/model/prompt_template on the wire and that
config.model is threaded into the pipeline as model_id
Also fixes the endpoints' user dependency: the lineage's
'Annotated[RequiredUser, Depends()]' double-wrap breaks on this branch's
FastAPI (CurrentUser got re-interpreted as query params -> every request
422'd); switched to the bare 'user: RequiredUser' style the rest of this
repo's routes use. Also isort-fixes the u9v0w1x2y3a4 migration touched by
the chain repair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* fix(pr-5): make rate-limiter fail-open path alertable (PR-4 follow-up)
The generation route wires rate-limit enforcement, but its route-level
fail-open path (Redis pool absent -> _get_redis returns None -> rate check
skipped) was silent, so ops could not alert on unmetered LLM traffic.
Now both fail-open layers log at WARNING: _get_redis narrows to
Import/AttributeError and warns on an absent pool, and the route logs the
bypass with user/project context when it skips the check. Behaviour is
unchanged (still fail-open; DB budget cap in step 5 remains the
non-fail-open backstop) -- only observability is added. Extends the
redis-unavailable route test to assert the WARNING fires.
Closes the PR-4 rate-limiter fail-open review follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* fix(pr-5): apply /ce:review findings — per-type suggestion parsing + provenance/robustness
BLOCKER: edges and annotations returned empty, validation-failing stubs and
dropped all type-specific payload — the pipeline parsed a single generic
raw["label"] that the edge/annotation prompts never emit. Replace with a
per-type dispatch (_parse_typed):
- edges: parse target_label/target_iri/relationship_type; flag non-controlled
relationship_type and null target with GEN-05 validation errors instead of
silently dropping; label = target_label.
- annotations: parse property_iri/value/lang; carry the focus class IRI;
display label falls back to value.
- parents (HIGH): link an existing parent by its IRI instead of minting a
duplicate node; correct IS-A direction (web links class_iri -> parent).
- siblings: parent to the focus class's shared parents, not to the class
itself; children unchanged.
Move the type-specific fields (property_iri/value/lang/target_iri/
relationship_type) onto the flat base GeneratedSuggestion so list[...] response
serialization carries them and the shape matches the web contract.
HIGH: refuse generation with 400 when config.model is empty (every suggestion
must carry a resolvable model id for provenance — D-08).
HIGH: rewrite the edge/annotation/parent tests with real prompt-output shapes
and assert the payload round-trips (the old fixtures injected a "label" key,
masking the blocker).
MEDIUM: unexpected pipeline exceptions now surface as 500 instead of being
masked as an empty-200; only transient TimeoutError/ConnectionError fail soft.
MEDIUM: NaN/inf confidence -> None (clamping had promoted NaN to 1.0).
LOW: 502 returns a generic client message (provider detail logged server-side
only, not echoed); dedup-unavailable logs an alertable distinct WARNING;
provider param is honestly typed (Optional + runtime guard, no type: ignore).
Full pytest 1688 passed; ruff + mypy(strict) clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* harden(pr-5): route rate-limit fail-open uses the unified alert marker
The generation route's Redis-pool-absent bypass now emits the same
FAIL_OPEN_EVENT=llm_rate_limiter_fail_open signal (with structured extra) as the
limiter-internal fail-open paths, so all three degraded paths alert as one event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* harden(pr-5): drop dead DuplicateCheckRequest.branch field
Duplicate detection always searches across ALL branches (DEDUP-08), so the
request's branch field was silently ignored. Remove it (nothing sends it — the
duplicate-check route lands in PR-6 with no branch consumer) and document why no
per-request branch scope exists. The candidate's found-on branch is still
reported via DuplicateCandidate.branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* feat(12-04): add duplicate-check API endpoint and implement all 7 tests
- New route: POST /projects/{project_id}/duplicate-check (DuplicateCheckResponse)
- Router registered in __init__.py alongside validation, llm, and other routes
- Replace 7 Wave 0 skip stubs with real async unit tests covering DEDUP-04/05/06/07/08 + D-01/D-09/D-13
- Tests mock semantic_search_all_branches + compute_similarity to control scores
- All 7 tests pass; full unit suite: 162 passed, 14 skipped
* harden(pr-6): access-gate the duplicate-check route + schema-parity entity_type
- Route had NO auth dependency in the lineage — any caller could run
duplicate checks (label + embedding similarity reads) against private
projects. Now mirrors /search/semantic: OptionalUser +
project_service.get (public readable by anyone incl. anonymous;
private requires membership; 403/404 before any scoring work).
- Re-added entity_type to DuplicateCheckService.check() — PR-5's carve
dropped it while DuplicateCheckRequest still advertises the field and
the route forwards it (would have raised TypeError at runtime).
Accepted-but-unused, documented as type-agnostic scoring.
- New tests/unit/test_duplicate_check_route.py (5 endpoint tests:
private-denied-before-check, 404, anonymous-public-read, request
wiring incl. entity_type, 422) + ruff fixes in the lineage test file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* test(pr-6): pin branch-field accepted-but-ignored contract (/ce:review MEDIUM visibility)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELAutiUDrKdGf2vZAwgt9Q
* test(pr-6): pin DuplicateCheckRequest.branch removal contract
Update the branch-ignored contract pin to assert the field is gone from the
request model (removed on PR-5) and that a stray branch key in the body is
ignored and never forwarded to the all-branches search.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eQ2CoAwpGv9Q8vBhuGGtH
* fix(llm): synthesize reviewed T3 hardening
* feat(trust): add contribution ladder and guarded auto-accept
* feat(dedup): add auditable distinct-entity decisions
* fix(pr): enforce open source-branch idempotency
* fix(review): preflight open PR uniqueness
* fix(pr): serialize lifecycle invariants
* feat(pr): expose retryable GitHub mirror status
* fix(review): harden GitHub mirror reconciliation
* fix(review): close API lifecycle race gaps
* feat(pr-party): data model and migrations (U1)
Four tables (reviewer, credential, pr, action) with the constraints that
retire the prototype's state bugs: (repo_full_name, pr_number) natural key
(pr_node_id is enrichment only), a partial unique index allowing one
non-failed action per (reviewer, pr, head_sha, kind) so failed actions are
retryable (C6) while replays collapse, and the pr_party_ready
once-per-revision partial index (R22). Notification and LLMAuditLog project
columns go nullable for the project-less surface (KTD11/KTD20/KTD17).
Live-PG round-trip and constraint behavior verified on a scratch DB.
* feat(pr-party): GitHub client layer with domain split and error taxonomy (U3)
Generation/actuation factory split (generation mode refuses writes before
any HTTP call — KTD13); non-PENDING review assertion with pinned commit_id,
merge sha with 409→StaleCardError, tri-state mergeability (null=computing),
check-runs rollup with an explicit NONE state, org search over issue-shaped
items, 401→TokenExpiredError / 422→SelfApprovalError / rate-limit taxonomy,
and token-expiration header capture for credential lifecycle (KTD13).
* feat(pr-party): reviewer registry, credentials, and capability surface (U2)
Config-driven registry (PR_PARTY_REVIEWERS reconciled at startup with
best-effort node-id resolution; empty config means unconfigured, never
delete-everything), MultiFernet credential storage with domain-separated
key derivation and validate-the-typed-token rotation (KTD12/KTD13),
/pr-party/me capability payload with credential + generation-token health,
settings routes for merge default and ntfy topic (secret, never in the
capability payload), and the AUTH_MODE=disabled router gate (KTD19).
Contributor-PAT retirement carve-out documented in user_settings.
* feat(pr-party): intake — org webhook receiver, two-stage sweep, cycle wiring (U4)
Sweep-first intake per KTD14: discovery search then detail/check fetches
only for changed rows, upserting on (repo_full_name, pr_number) with strict
column ownership (brief columns preserved across refresh — the B3 class).
HMAC-verified org webhook with delivery-id dedupe converging on the same
upsert path; author classification against the registry (counterpart =
any principal, own derived per-caller at read time — model docstring
aligned); 90-minute brewing timeout to ready-with-warning (R17);
missing_since stamping with consecutive-miss tolerance (C7); conditional
arq cron; API-side Redis settings unified with the worker's DSN handling.
* feat(pr-party): tool-denied brief generation worker (U5)
arq task (max_tries=1, explicit timeout) generating structured plain-string
briefs through the generation-domain client only — tool-denial asserted at
the seam and mutation-tested (A1/KTD17). Untrusted-data delimiters, diff
cap with truncation flag, CE artifacts fetched only as own-repo contents
paths (SSRF-constrained), server-side links allowlist, instruction-echo
rejection with one retry, fail-closed daily budget in Redis, approved-
provider gate, audit with null project. Brief failure still releases the
card (ready_at stamped; links render from PR facts).
* feat(pr-party): queue and card read API (U15)
Reviewer-gated read surface with per-caller author projection (own derived
from stored identity, node-id-first), server-computed readiness with plain-
language reasons (R17/KTD19), head-scoped other-reviewer state and staleness
(R24), discuss-live park derivation pinned as the U6 contract, plain-string
brief serialization (R21), deep links from PR facts, Cache-Control:
no-store on both endpoints. 403s carry no PR data; 401 unauthenticated.
* feat(pr-party): PR title flows GitHub -> intake -> queue cards (U15 follow-up)
Nullable title column (migration amended in place; branch is local-only),
written by both intake stages with truncation, serialized on queue and card
payloads. Adds a DDL-equals-model drift test for pr_party_pr.
* feat(pr-party): verdict endpoint with durable idempotency and degraded fallback (U6)
KTD16 implemented exactly: server-side readiness re-check with override
recording (R26/AE6), pre-flight from the server-side card row only (A4),
pending-insert-before-GitHub-call with replay/in-flight/reclaim/re-open
semantics (C2/C6), merge as its own claim authorized by an approval at the
current head (R18/R11), discuss-live parks without GitHub (R9) plus unpark,
TokenExpired -> degraded intent with deep link and credential marking (R12),
scrubbed error storage, and a fail-closed daily action limiter (actuation
503s on Redis loss; credential PUT deliberately fails open).
* feat(pr-party): ready notifications — in-app bell plus ntfy ping (U9)
One pr_party_ready notification per routed reviewer per PR revision,
enforced by the U1 partial unique index via SAVEPOINT no-op (R22); author
excluded node-id-first; target_url is the card deep link and the ntfy
payload carries a fixed title and link only — no PR-derived text (KTD20/
R27). Notification schemas accept null project fields so a mixed bell page
serializes (the F5 regression case). Hook registered idempotently in both
the API lifespan and the arq worker startup.
* feat(pr-party): Q&A, deliberation notes, and the contained org answerer (U7)
Questions, notes (R14), and CodeRabbit re-triggers post as the reviewer via
the actuation domain, with compose-for-copy degraded fallback (R12/KTD18);
answers bind only by linkage, never author identity (C4); qa_thread serves
live from GitHub comments through the generation client (KD5 — GitHub stays
canonical; questions persist no app rows). The org answerer workflow ships
as a carried asset for a catholicos/.github fork PR: OWNER/MEMBER gate,
least-privilege permissions, no PR-head checkout, single-comment allowed
tools — all yml-asserted in tests (A2/KTD18).
* feat(pr-party): reconciler — convergence with live GitHub state (U8)
Third sweep stage: abandoned pending reviews back-fill from a matching
GitHub review (C2 healing, no duplicate post, commit_id-scoped per C1);
degraded intents confirm by reviewer node id (rename-proof, C3) with
derived nag state; dismissed approvals flip their action row failed so
merge authorization loses them (R20); missing_since rows verify via detail
fetch before retiring (C7 eventual-consistency); closed/merged projections
archive by row deletion after 14 days. Per-item isolation keeps a broken
repair pass from costing the committed facts refresh.
* fix(pr-party): apply code-review round — actuation, reconcile, intake, webhook, containment
Review fixes: reclaimed pending reviews adopt an existing GitHub review
instead of re-posting; back-fill and degraded confirmation are verdict/
state-aware; degraded-intent replay retries once a credential is repaired
and always carries the deep link; httpx transport errors join the error
taxonomy; scrubbed error prose everywhere credentials store it; DB-cascade
via passive_deletes; pr_id index; discovery page-cap reports incomplete;
failed webhook handlers release the delivery claim for redelivery; stale
snapshots cannot roll facts backwards; HMAC compares bytes (non-ASCII
header 401s); org answerer delimiters are per-run nonce-suffixed; sweep
cron gets an explicit timeout. Consolidations: shared split_repo,
default_generation_client, parse_dt/to_int, emit_ready_transition,
CredentialResolver/mark_credential_dead; ReviewerReconcileResult rename.
* fix(pr-party): cross-model review round — atomic action claim and per-run brief delimiter
The first independent-model pass over this branch (Codex, adversarial scope)
found two issues the eight in-family personas missed, plus the recorded API
residuals B6/B9/B12/B13/B14/B15/B16/B17 and A2.
- N1: existing action rows are now claimed with SELECT ... FOR UPDATE held
through the pending commit. Without it, two requests could read the same
stale pending, failed, or degraded-intent row, both flip it to pending, and
both call create_review — posting two real reviews on a colleague's PR. The
partial unique index cannot catch this because both transactions UPDATE the
same row, so it is distinct from B12's first-INSERT race. Concurrency tests
cover all three retry paths.
- N2: brief prompts mint an unpredictable per-run delimiter after context
collection and neutralize delimiter-like text case-insensitively. The fixed
lowercase tokens could be forged with case variants that read as the same
boundary to an LLM.
- B9: sweep and reconcile roll the shared session back per item, so a flush
failure no longer poisons every subsequent item with PendingRollbackError.
- B12: unique live-fingerprint violations map to the intended 409 instead of a 500.
- B14: webhook events for repos outside pr_party_org are ignored before processing.
- B15: app-origin links no longer admitted from LLM output; origins compared by
parsed host and port rather than a reconstructed string.
- B17: PRFacts.mergeable_known stops a webhook's asynchronous null from
overwriting sweep-established mergeable_state.
- B13: dismissal fallback prefers a standing review over feed ordering.
- B16: action ordering matches its documented verdict-first contract.
- B6: the migration's downgrade deletes only pr-party rows, not every
NULL-project audit row.
- A2: draft, merged, and closed refusals each say what actually happened.
Gates: 2547 tests green, mypy strict clean, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkjoGjpJCeTFqnu1eMiZp4
* fix(llm-review-r2): 2 — degrade unavailable brief pricing
* feat(ops): add safe PR Party credential rewrap (D5)
* fix(pr-party): align replay tests with synthesis topology
* style(pr-party): format synthesized T5 files
* feat(translations): provenance store — translation_records model, migration, value-hash util (U1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): per-instance settings, palette, config routes (U2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): primary provider/model on translation config (U2 ext)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): consensus/confidence engine with scoring, egress, metering contracts (U3)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): compact owl:Axiom provenance annotations with record digest (U4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): gated auto-commit path — shared branch lock, apply-time revalidation, split author/committer, coalesced index refresh (U5)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): coverage + provenance queries under annotation-plus-hash rule (U8)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): mint trigger, label-diff jobs, on-demand endpoint, abuse controls (U6)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): native-speaker reviewer tags + object-scoped confirmation API (U9)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* feat(translations): backfill with per-call cost preview, era scoping, one-active-job lifecycle (U7)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* test(translations): real-seam integration proof AE1-AE5 + in-flight race; fix 4 integration bugs it caught (U14)
Fixes found by the proof: async lazy-load MissingGreenlet in translation jobs
and confirmation route; provisional gate ignored at commit eligibility;
machine commit identities wrong at the author/committer seam.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* refactor(translations): simplification pass — slot-indexed coverage, era fast path, single-hash labels, shared BCP 47 validation (ce-simplify)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* fix(translations): review fix wave — advisory-lock parity, confirm/reject revalidation, scoring floors, quota fan-out, era idempotency, lifecycle hardening, canonical serialization (ce-code-review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNjtnCqGME93jF6xQZNH7q
* fix(types): satisfy pyright on translation modules (base gate now enforced)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(translations): chain T6 migrations after T5
* style(translations): format synthesized T6 files
* fix(translations): close metering and reviewer gaps
* fix(index): stop dropping altLabel, prefLabel and title from class detail
get_class_detail had two queries that were meant to be complements and were not:
- the labels query returned ONLY rdfs:label
- the annotations query excluded ALL of LABEL_PROPERTIES, commented "already
returned via IndexedLabel"
LABEL_PROPERTIES holds five entries: rdfs:label, skos:prefLabel, skos:altLabel,
dcterms:title and dc:title. The "already returned" claim was true only for
rdfs:label. The other four were excluded from annotations and never added to
labels, so they fell through both and vanished from the response.
Downstream this was destructive, not merely lossy: ontokit-web regenerated the
class Turtle block from the partial payload, so the four properties were deleted
on disk. One observed edit on DEV removed 13 skos:altLabel values across nine
languages while the user was changing a single comment.
The exclusion set is now exactly the properties this response genuinely returns
elsewhere — rdfs:label and rdfs:comment — defined once beside the queries that
justify it, with the invariant stated: excluded from annotations iff returned
elsewhere in this response. LABEL_PROPERTIES is unchanged; it remains correct for
label resolution and preference ordering.
Only the indexed path was affected. The RDFLib fallback iterates
ANNOTATION_PROPERTIES, which already includes these, and was correct — which is
precisely why this survived testing: cold-start passes, a warm index loses data.
Adds a parity test asserting both paths return the same predicate/value/language
set for the same fixture. That test, not the one-line fix, is the real regression
guard. A known remaining divergence for predicates outside ANNOTATION_PROPERTIES
(e.g. skos:related) is tracked as CatholicOS/ontokit-api#212.
Both tests fail before this change and pass after.
Part of CatholicOS/ontokit-web#361.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q15MzMCFSEu2YYKczpmum3
* fix(ontology): unify annotation classification paths
* feat(audit): snapshot columns on suggestion_outcomes — migration + model (U1)
Six nullable no-default columns (tier, role, submitter/decider display
identity, captured-at discriminator), keyset audit index, non-destructive
downgrade. R3/R4/R9, KTD2.
Co-Authored-By: Codex via ce-work cross-model <noreply@openai.com>
* feat(audit): capture submitter snapshot at the record_outcome seam (U2)
Tier/role resolved from the threaded project (no second fetch), keyed on
the session submitter; captured_at written unconditionally; identity
copied for every submitter; non-throwing degrade with PII-free warning.
R1/R2/R5/R6/R8, KTD1/KTD3. Covers AE1-AE4, AE6/AE7 at unit level.
Co-Authored-By: Codex via ce-work cross-model <noreply@openai.com>
* test(audit): preserve bulk review query contract
* feat(audit): trust outcomes audit endpoint with keyset pagination (U3)
GET /{project_id}/trust/outcomes — owner/admin gated, cursor on
(created_at, id) desc, explicit project_id predicate (tenant isolation),
{items, total, next_cursor}, values served as stored. R4/R7/R8, KTD5.
Co-Authored-By: Codex via ce-work cross-model <noreply@openai.com>
* test(audit): real-seam integration proof AE1-AE7 (U4)
Migrated live Postgres + real services: reject/accept snapshots, sweep
path, anonymous attribution, pre-feature nulls, pre-promotion tier at
threshold crossing, degraded capture, authz + tenant isolation.
Co-Authored-By: Codex via ce-work cross-model <noreply@openai.com>
* fix(audit): review fix wave — role width, pure-refusal downgrade, cursor proof, contract docs (ce-code-review)
snapshot_role widened to String(50) matching ProjectMember.role (codex
peer finding — DB-valid long role aborted the outcome flush post-merge);
downgrade raises before any DDL (drop_index was rolled back by its own
raise); redundant post-commit project reload removed and project_id
derived from the threaded project; next_cursor documented as the sole
end-of-list signal; degradation log carries exception class name only;
real two-page cursor walk + tie-breaker, deeper malformed-cursor
branches, editor-role 403, AE7 caplog + anonymity distinctness.
Co-Authored-By: Codex via codex MCP <noreply@openai.com>
* fix(audit): bind cursors to their project
* style(audit): format synthesized audit files
* fix(audit): preserve bulk approval project contract
* feat(demo): prepare credential-separated refresh
* feat(demo): isolate resettable project targets
* fix(demo): expose provisioned ontology availability
* fix(demo): expose exact resettable repository (U9)
* fix(demo): align synthesis migration and contracts
* fix(review): bound demo project discovery
* fix(review): harden demo activation lifecycle
* fix(ci): migrate integration database before tests
* ci: make codecov uploads best-effort
* style: apply current Ruff formatting
* fix(types): snapshot LLM budget limits at route boundary
* fix(types): separate strict suggestion constructors
* fix(types): bind pinned DNS backend to interface
* fix(ci): align mypy with Python 3.13
* fix(db): allow reviewed suggestion session states
* test(translations): patch reservation rate-limit seam
* security(actions): pin Claude answerer commit
* security(sql): document embedding query invariants
* security(logs): minimize identity failure detai…
Clears both
DeprecationWarnings the test suite was emitting after the dependency bumps in #215. Neither changes behaviour.HTTP_413_REQUEST_ENTITY_TOO_LARGE→HTTP_413_CONTENT_TOO_LARGEStarlette 1.3.1 deprecated the old spelling. Both constants resolve to
413, and there is a single call site — the upload size guard inimport_project(ontokit/api/routes/projects.py:197). The test assertingresponse.status_code == 413is unaffected.starlette.testclient→httpx2starlette.testclientnow importshttpx2and falls back tohttpxwith a warning. Addinghttpx2to the dev group makes TestClient pick it up:httpxdeliberately stays a runtime dependency —core/auth.py,services/github_service.py,services/user_service.py,services/embedding_providers/openai_provider.pyandservices/sitemap_notifier.pyall use it directly, andtest_auth_routes.py/test_github_service.pymockhttpx.Responseandhttpx.HTTPStatusErroragainst that runtime code.Verification
ruff check,ruff format --check,mypystrict,pyright— all clean (0 errors)uv lock --checkconsistentSummary by CodeRabbit