Release candidate. Docker :latest and a plain pip install stay on 0.9.9 until 0.10.0 ships; nothing moves unless you opt in. We would like real-world runs before the stable tag — see "What we'd like you to try" below.
OPAL 0.10 moves from pydantic v1 to pydantic v2 and from Starlette 0.x to 1.x. Everything else in this release follows from that move.
Why
CVE-2026-48710 (Starlette, request.url built from an unvalidated Host header) is fixed in Starlette 1.0.1, and OPAL's starlette<1 pin made no patched version reachable. OPAL was never exploitable through it — there is no use of request.url or request.base_url anywhere in the codebase; auth is header-based and authorization reads JWT claims and routed path parameters. This is dependency hygiene to clear scanners, not incident response, which is why it ships as a proper 0.10 with an rc rather than as a patch.
Starlette cannot move alone: Starlette 1.x needs FastAPI ≥ 0.133, and FastAPI has required pydantic 2 since 0.126. So one CVE bump became a pydantic major.
Moving to starlette>=1.3.1 also clears GHSA-82w8-qh3p-5jfq (HIGH), GHSA-wqp7-x3pw-xc5r (HIGH), GHSA-x746-7m8f-x49c and GHSA-jp82-jpqv-5vv3; setuptools>=78.1.1 clears GHSA-5rjg-fvgr-3xxf (HIGH).
How to opt in
Docker — every image is published under the 0.10.0-rc.1 tag (plus -alpine):
permitio/opal-server:0.10.0-rc.1
permitio/opal-client:0.10.0-rc.1
permitio/opal-client-standalone:0.10.0-rc.1
permitio/opal-client-cedar:0.10.0-rc.1
permitio/opal-client-eopa:0.10.0-rc.1
pip — pre-releases are skipped unless you ask for them:
pip install --pre opal-client # or: opal-client==0.10.0rc1
pip install --pre opal-server # Python 3.10–3.12 only, see below
Breaking changes
1. pydantic v2 is required
opal-common, opal-client and opal-server now require pydantic>=2.9,<3 (and fastapi>=0.135, starlette>=1.3.1,<2). If you embed opal-client or opal-common as a library in an application that still pins pydantic<2, the two can no longer share an environment. This is the one change with no workaround on OPAL's side.
2. Custom fetch providers must be pydantic v2 compatible
Fetch-provider configs and events are pydantic models, so they move with OPAL. Migration guide: Writing your own fetch provider. The short version:
| pattern in your provider | what happens under 0.10 |
|---|---|
bare @root_validator
| raises at import (PydanticUserError) — your module fails to load. Use @model_validator(mode=...), or @root_validator(pre=True) / @root_validator(skip_on_failure=True) as a bi-compatible stopgap
|
Field(regex=...), Field(const=...), Field(unique_items=...)
| raises at import — use pattern=, Literal[...], and a validator respectively
|
some_field: Optional[X] with no default
| required at runtime — v1 defaulted it to None; add = None
|
class Config with orm_mode, allow_population_by_field_name, schema_extra
| silently ignored — rename to from_attributes, populate_by_name, json_schema_extra
|
@validator, inner class Config, .dict(), .json(), .parse_obj()
| still work through pydantic's deprecation shims (removed in pydantic 3) |
Two cases are handled on OPAL's side and need no change: passing a config model where dict is declared, and a provider event that re-declares config with its own config type — the instance is preserved intact, including subclass-only and alias-populated fields.
3. Numbers are no longer coerced to strings on data-update endpoints
POST /data/config and POST /scopes/{scope_id}/data/update now reject numeric values in string-typed fields with a 422: reason, and per entry url, dst_path, save_method and each item of topics. A numeric reason (a ticket or revision number) is the most likely to bite. pydantic v2 does not coerce, and we chose not to restore v1's leniency. Cast before sending. Details: Triggering data updates, External data sources.
4. Python versions
- Floor is 3.10 for all three packages. 3.9 was metadata only — the shipped images have been on
python:3.10for a long time — so no running deployment loses support. opal-clientandopal-commonnow install on Python 3.13. The previous<3.13ceiling was justified by an internal that lives only inopal-server.opal-serverstays at<3.13: its pinnedpygit2has no Python 3.13 wheel yet. Note that on 3.13,pip install opal-serverdoes not fail — pip silently picks the newest release whose metadata admits 3.13, which is the old 0.9.6. Run the server on 3.12 or lower. Details: Run OPAL as Python packages.
5. Validation error (422) bodies use pydantic v2 wording
Error entries keep the loc / msg / type keys, but the strings are pydantic v2's: "Field required" instead of "field required", type missing instead of value_error.missing, and so on. Anything that matches on error text should be re-checked. The input, ctx and url keys pydantic v2 adds are stripped by OPAL, so a rejected payload — which can carry data-source credentials — is never echoed back.
6. Dependency changes
| package | 0.9.9 | 0.10 |
|---|---|---|
pydantic[email]
| >=1.9.1,<2
| >=2.9.0,<3
|
starlette
| >=0.40.0,<1
| >=1.3.1,<2
|
fastapi
| >=0.109.1,<1
| >=0.135.0,<1
|
fastapi_websocket_pubsub[all]
| >=1.0.0,<2
| >=1.0.1,<2
|
fastapi_websocket_rpc
| >=0.1.28,<1
| >=0.1.29,<1
|
setuptools
| >=70.0.0
| >=78.1.1
|
fastapi-utils, typing-inspect
| present | removed — fastapi-utils was a dead import, and it transitively held psutil<6; psutil now resolves to 7.x and the Alpine images no longer build it from source
|
Compatibility with 0.9.x
A 0.10 server and 0.9.x clients work together, and so does the reverse. The wire format is unchanged: a corpus of 33 payload cases recorded under pydantic v1 is checked byte-for-byte against 0.10 in both dump and parse directions, including timedelta fields (still float seconds, not ISO durations) and the token ttl. Redis-persisted Scope records round-trip unchanged, so an existing server backbone needs no migration. The OpenAPI contract of both apps was diffed against 0.9.9: identical routes, identical schema set, nothing newly required. CI runs a mixed-version end-to-end suite with a 0.9.6 client against the new server.
Recommended order for a fleet: server first, then clients — but nothing forces it.
Also in this release
- Python 3.13 CI leg for
opal-commonandopal-client, installed with pip so theRequires-Pythonmetadata is actually enforced. - A wire-compatibility test harness (
opal_common/tests/wire_compat/): golden corpus, enum-field inventory that forces a decision for every enum-bearing model, Redis round-trip, real-OPA-binary argv checks across the Python matrix, and a fetch-provider conformance sweep over eleven v1-era provider shapes. - Docs: pydantic v2 migration section for fetch providers, breaking-change callouts on the data-update pages, per-package Python ceiling in the install guides.
What we'd like you to try before 0.10.0
- Custom fetch providers — load them against
opal-client:0.10.0-rc.1and watch for import errors and forOptionalfields that used to default toNone. - Services that post data updates — any integration calling
/data/configwith numericreasonortopicsvalues. - Library embedding —
opal-client/opal-commoninside your own app, especially on Python 3.13. - Alpine images —
psutilmoved from a source build to a wheel; worth a boot check. - Mixed fleets — a 0.10 server in front of your existing 0.9.x clients, or the reverse.
Report anything at https://github.com/permitio/opal/issues and mention 0.10.0-rc.1.
Credits
Migration started by @EliMoshkovich; review by @zeevmoney and @dshoen619, whose findings drove most of the test harness.
Full changelog: 0.9.9...0.10.0-rc.1 — #959, #960.