Say when a route needs the container PDP, document which proxied writes wait, send X-Wait-Timeout: 0 - #142
Draft
zeevmoney wants to merge 26 commits into
Draft
Say when a route needs the container PDP, document which proxied writes wait, send X-Wait-Timeout: 0#142zeevmoney wants to merge 26 commits into
zeevmoney wants to merge 26 commits into
Conversation
facts_sync_timeout=0 and wait_for_sync(timeout=0) sent no X-Wait-Timeout header, because the header was set only for a truthy timeout, so the PDP waited its own default instead of answering without waiting. The header is now sent whenever the timeout is not None (PER-16681). X-Timeout-Policy keeps its check: none of its values is falsy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_user_tenants() now builds its 404 message with a helper that the other container-only routes can share. The text is unchanged; a new test pins it exactly. SETUP_PDP_DOCS_LINK moves next to the helper and stays importable from permit.enforcement.enforcer. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cloud PDP answers 404, with an empty body, for the /facts routes
that proxy_facts_via_pdp sends facts to and for the /local routes of
permit.pdp_api. The SDK raised that as a bare "404 API Error". It now
raises a PermitApiError, the type it raised before, whose message names
the route and says only the container PDP serves it; the error's
details hold the message too.
A 404 counts as the cloud PDP's when the pdp setting is the cloud PDP's
host, or when its body is empty. A container PDP's own 404s have a JSON
body, and so do the API's 404s its /facts routes pass on, so a missing
tenant through a container PDP still raises PermitNotFoundError with
its usual message.
PermitApiError takes an optional keyword-only message for this.
The new tests send PATCH /facts/users/{user_id}, so its "untested"
API coverage allowlist entry goes.
Refs PER-16340.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every facts method that proxy_facts_via_pdp sends to the PDP now says it is container PDP only with the proxy on, and that the cloud PDP's 404 is raised as a PermitApiError that says so. pdp_api's role_assignments.list() and get_user_tenants() say it plainly, and the pdp_api entry points say the cloud PDP serves none of its routes. A test pins the list of facts methods against the public methods of the facts APIs, and checks each docstring on both clients. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With proxy_facts_via_pdp on, each method of the users, tenants, role assignments, resource instances and relationship tuples APIs now has an offline wire test that pins the route it sends its request to and that the request carries X-Wait-Timeout and X-Timeout-Policy (PER-16339). The test holds the set of facts routes the PDP waits on before it answers. Those are the only /facts operations the PDP's spec lists, and a test keeps the set equal to the committed copy of that spec. The API coverage allowlist drops the five PDP operations these tests now cover, and lists the other proxied routes the PDP forwards without listing them in its spec (PER-16338). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PDP waits until it has a facts write before it answers on 11 routes only, and forwards every other facts request without waiting. The docstring of wait_for_sync(), the descriptions of proxy_facts_via_pdp and facts_sync_timeout and a new README section, "Read-your-writes through the PDP", now list the methods it waits on. The README also lists the writes it does not wait on, and says what a timeout of 0 or None and each timeout policy do (PER-16339). A test reads the four lists and checks that each names exactly the methods whose route the PDP waits on, as the offline wire tests pin them, so the docs cannot drift from the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client created with proxy_facts_via_pdp on and the cloud PDP's host as its pdp sends every facts request to /facts routes the cloud PDP does not serve. Its creation now issues a UserWarning that says so, attributed to the line that created the client, on the async and the blocking client alike, and past a subclass's super().__init__(). It is issued at each creation; Python's default filter shows it once per line. A client that wait_for_sync() yields does not issue it. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The container_pdp fixture, which skipped the get_user_tenants e2e tests when the PDP in use is the cloud PDP, moves to tests/conftest.py with a reason that covers every container-only route. The e2e tests that call permit.pdp_api or write facts through proxy_facts_via_pdp use it too, so a run with CLOUD_PDP=true, or PDP_URL set to the cloud PDP, reports them as skipped instead of failing on the cloud PDP's 404. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests join the get_user_tenants one in the e2e (cloud PDP) job: permit.pdp_api.role_assignments.list() and, with proxy_facts_via_pdp on, users.get() raise a PermitApiError that names the route and says only the container PDP serves it. The second also checks the warning at the client's creation. Both only read, so they write nothing even if the cloud PDP ever serves those routes. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture maps the cloud PDP's host to the local test server through socket.getaddrinfo, which aiohttp uses unless aiodns is installed. It now asserts that, instead of letting a test reach the real host. Refs PER-16340. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raise a container-PDP message when a facts route under proxy_facts_via_pdp or a pdp_api route gets the cloud PDP's 404, say "container PDP only" in the affected docstrings, warn at client creation when the facts proxy points at the cloud PDP, and skip the container-only e2e tests on the cloud PDP job (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Send X-Wait-Timeout whenever the facts sync timeout is not None, so 0 means do not wait (PER-16681), and document which proxied facts writes the PDP waits on, with a test that pins every facts method's route and sync headers and keeps the docs in step (PER-16339). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The table of every public facts method and the one request it sends with proxy_facts_via_pdp on moves from test_facts_sync_offline.py to tests/facts_methods.py, and the container-PDP-only tests use it too: - the cloud PDP's 404 is now checked for the 47 methods whose request goes to the PDP, not 14, so users.get_assigned_roles' own client and each route of every facts client are covered; - the docstring pins take the methods that say "container PDP only", and those that must not, from the same table, in place of a list typed by hand and a second check of the facts APIs' methods; - Case gives a request's HTTP method and its sent() form, which both modules used to build inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's read-your-writes section and the proxy_facts_via_pdp description said only that the cloud PDP answers 404. They now say that the SDK raises that 404 as a PermitApiError naming the route, and that a client created with the facts proxy on and the cloud PDP as its pdp issues a UserWarning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CONTRIBUTING.md's API coverage section now says that tests/facts_methods.py pins the request of every facts method with proxy_facts_via_pdp on, that the wait and cloud-PDP 404 tests run on each, and that a new facts method needs a case there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cloud PDP answers a route it does not serve with a 404 of zero bytes. A 404 whose body is only whitespace now keeps the error it raised before, as the docstring already said, and a test pins it on a facts route and a pdp_api route (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cloud PDP serves none of the routes only the container PDP serves, so every 404 it sends for one is a missing route. Say so where the rule is defined, so that a cloud PDP that starts to serve one of them gets its real "not found" told apart there (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warning at creation fires for any address on the cloud PDP's host, whatever its scheme, port, path or letter case, and not for an address on another host that only mentions it, one with no host, or one that cannot be parsed (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error for the cloud PDP's 404 on a route only the container PDP serves keeps the body's text under "details" and adds the message under "message", whatever the body. Say so in the body argument's docs (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The warning at creation said what to do in other words than the 404 error does. It now ends with the same advice as that error, and with the link to the PDP setup docs (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
creation_site() read its caller's frame with sys._getframe(1), which needed a lint suppression. inspect.currentframe() and f_back reach the same frame through the public API (PER-16340). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency Security AuditScanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major) ✅ No known vulnerabilities found. Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With X-Wait-Timeout: 0 the PDP's wait times out at once, so the policy decides the answer: "ignore" returns the write's response and "fail" answers 424 for every write that waits. The docs said only that 0 makes the PDP answer without waiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
added this pull request to stack #145
October 2, 2026 18:24
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear issues
X-Wait-Timeoutwhen the facts sync timeout is 0.Stacked on #141.
Why
/healthonly. It answers 404 for the/factsroutes thatproxy_facts_via_pdpsends facts to, for the/localroutes ofpermit.pdp_api, and for/user-tenants. For the first two, the SDK raised a generic404 API Error: {'details': ''}, which does not say that the route needs the container PDP.wait_for_sync(),facts_sync_timeoutandproxy_facts_via_pdpread as if every proxied facts write waits for the PDP to have the change. The PDP waits on 11 routes and forwards every other facts request without waiting.facts_sync_timeout=0andwait_for_sync(timeout=0)sent noX-Wait-Timeoutheader, because the SDK checked the value's truthiness. The PDP then waited its default of 10 seconds instead of not waiting.What changed
Routes only the container PDP serves (PER-16340)
permit/utils/cloud_pdp.pydecides when a 404 on a container-only route is the cloud PDP's. It is when thepdpURL's host iscloudpdp.api.permit.io(any scheme, port, path or letter case), or when the 404 body is empty (zero bytes). Every other 404 keeps its current error and message: a container PDP's JSON 404, and the API's 404 that a container PDP's/factsroutes pass on, such as for a missing tenant (PermitNotFoundError).SimpleHttpClienttakes a keyword-onlycontainer_pdp_advice. Every facts client underproxy_facts_via_pdpand everypermit.pdp_apiclient sets it. On the cloud PDP's 404, these clients raise aPermitApiErrorwhose message names the route, followed by what to do and the PDP setup docs link. For example:The SDK got status code 404 from the PDP at https://cloudpdp.api.permit.io: only the container PDP serves POST /facts/users, and the cloud PDP does not.PermitApiErrortakes a keyword-onlymessage.get_user_tenants()keeps thePermitConnectionErrorand the exact message it got in Add tenants.create_user and get_user_tenants #139. The message is now built by the same helper.proxy_facts_via_pdp=Trueand apdpon the cloud PDP host issues aUserWarning:wait_for_sync()yields do not issue it;pdp_api.role_assignments.list()andget_user_tenants(). The sync stub is regenerated.container_pdpskip fixture moves totests/conftest.py. The five e2e tests that need a container PDP use it.e2e (cloud PDP)job gets two read-only tests that check the new 404 messages and the warning.Which proxied writes wait for sync (PER-16339)
wait_for_sync()docstring, theproxy_facts_via_pdpandfacts_sync_timeoutfield descriptions, and a new README section, "Read-your-writes through the PDP". They also say that every other facts request is forwarded without waiting. The 11 methods areusers.create,users.update,users.sync,users.assign_role,users.unassign_role,tenants.create,role_assignments.assign,role_assignments.unassign,resource_instances.create,resource_instances.updateandrelationship_tuples.create. The README also lists the 17 writes that are forwarded without waiting.tests/facts_methods.pyholds a table of all 48 public facts methods, with the exact request each one sends under the proxy.tests/test_facts_sync_offline.pyuses it to check that:X-Wait-Timeout, or not, to the PDP route the table gives;/factsoperations in the committed PDP spec (.github/api-specs/pdp.json);sdk_onlyentries (PER-16338) are added for proxied routes that the PDP forwards without listing them in its spec.Timeout 0 (PER-16681)
X-Wait-Timeoutwheneverfacts_sync_timeoutis notNone, 0 included.X-Timeout-Policyis unchanged, because none of its values is falsy.Behaviour changes
proxy_facts_via_pdp=Trueand everypermit.pdp_apimethod still raisePermitApiError, but with a new message that names the route and says it needs the container PDP.err.detailschanges from{'details': ''}to{'details': '', 'message': <message>}.err.__cause__is nowNone; it wasaiohttp.ContentTypeError.get_user_tenants()are unchanged.UserWarningat client creation whenproxy_facts_via_pdp=Trueandpdpis on the cloud PDP host. With warnings turned into errors, creating such a client raises.facts_sync_timeout=0sendsX-Wait-Timeout: 0.0, andwait_for_sync(timeout=0)sendsX-Wait-Timeout: 0. Before, no header was sent, and the PDP waited its default: 10 seconds, unlessPDP_LOCAL_FACTS_WAIT_TIMEOUTsets another.Additions to the API that existing callers don't need to change for:
PermitApiError(..., message=...)andSimpleHttpClient(..., container_pdp_advice=...).How it was tested
Offline suite: 1166 passed, 3 skipped, 0 warnings on both the pydantic-v2 and pydantic-v1 lanes. The base has 787 passed, 3 skipped. The new tests are in
tests/test_container_pdp_only_offline.py(251) andtests/test_facts_sync_offline.py(128).How the wire tests work: each one runs on the async and the blocking client against a local
pytest-httpserver. It asserts the exact method, path, query, headers and body, and that the other server got nothing. Clients are closed after each call.Mutation checks: 26 mutants of the code these tests guard were run at the final head, covering:
wait_for_sync;25 were caught. The one survivor is equivalent to the original: it starts the frame walk one frame further out, which the
__init__skip makes identical.e2e: 44 tests collect. They need keys and run in CI. With a dummy key and unreachable services, the five container-only e2e tests skip with their reason.
Other checks:
Owner actions before merge
e2eande2e (cloud PDP). The cloud PDP job now runs 8 tests, two of them new. If one of the new ones fails, the cloud PDP answeredGET /local/role_assignmentsorGET /facts/users/<key>with something other than a 404.🤖 Generated with Claude Code