github permitio/opal 0.10.0-rc.1

pre-release4 hours ago

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.10 for a long time — so no running deployment loses support.
  • opal-client and opal-common now install on Python 3.13. The previous <3.13 ceiling was justified by an internal that lives only in opal-server.
  • opal-server stays at <3.13: its pinned pygit2 has no Python 3.13 wheel yet. Note that on 3.13, pip install opal-server does 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 removedfastapi-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-common and opal-client, installed with pip so the Requires-Python metadata 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.1 and watch for import errors and for Optional fields that used to default to None.
  • Services that post data updates — any integration calling /data/config with numeric reason or topics values.
  • Library embeddingopal-client/opal-common inside your own app, especially on Python 3.13.
  • Alpine imagespsutil moved 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.

Don't miss a new opal release

NewReleases is sending notifications on new releases.