Repository navigation
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
| @@ -0,0 +1,3 @@ | |||
| # AI Agent Guidelines for auth0-api-python | |||
|
|
|||
| @./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries. | |||
There was a problem hiding this comment.
The @./CLAUDE.md import is Claude Code syntax. Other tools that read AGENTS.md may show the line as plain text and never load CLAUDE.md. There is also a feat/add-agents-file branch on the repo with a different AGENTS.md (a guide for integrators), so it might be worth checking the two don't clash.
There was a problem hiding this comment.
The feat/add-agents-file branch is very old and stale. Better to use the new one as a cleaner approach.
There was a problem hiding this comment.
https://developer.android.com/studio/gemini/agent-files#modularize-agent-files Gemini does support the syntax @./CLAUDE.md. Codex doesnt call out this syntax as supported there was open gh issue too on this, somehow using codex cli ,llm was able to infer it and it made a read tool call just as how reference file work and not as inline instructions when i tested it..
I think we would anyways have single agents.md from now on we would not need to maintain multiple files @rmad17 you can raise a new PR after this with only agents.md. i have also updated the skill to only generate agents.md
Co-Authored-By: Claude <noreply@anthropic.com>
| utils.py # normalize_domain, OIDC/JWKS fetch, unverified header/payload decode, htu normalization | ||
| act.py # get_current_actor / get_delegation_chain (RFC 8693 act claim) | ||
| types.py # TypedDicts: DomainsResolverContext, OnBehalfOfTokenResult, ClientCredentialsTokenResult, ActClaim. DomainsResolver callable alias | ||
| token_utils.py # Test helpers (token/DPoP proof generators) with a synthetic test key. Not in __all__, but packaged and importable |
There was a problem hiding this comment.
telemetry.py (added in #153, it builds the Auth0-Client and User-Agent headers and validates client_info) isn't in this list of source files.
Shall we add a line for it?
| token_utils.py # Test helpers (token/DPoP proof generators) with a synthetic test key. Not in __all__, but packaged and importable | ||
| tests/ | ||
| conftest.py # Shared fixtures (api_client_confidential, mock_discovery) and httpx mock helpers | ||
| test_api_client.py, test_cache.py, test_utils.py, test_act.py, test_token_store.py |
There was a problem hiding this comment.
tests/test_telemetry.py is missing from this list, and the conftest line above doesn't mention the new auth0_client_payload and assert_telemetry_headers helpers. The test lists in testing.md and flow-map.md, and the helper list in testing.md, have the same gap.
Shall we update them together?
| - Make surgical changes. Touch only what the request requires. Do not refactor or reformat adjacent code. | ||
| - Follow existing style and naming. Add new public `ApiClient` methods after the existing public methods, above the `# ===== Private Methods =====` banner in `api_client.py`. Add new private helpers below it. Never reorder existing declarations. | ||
| - Add unit tests for new functionality, marked `@pytest.mark.asyncio` when async, mocking HTTP with `pytest-httpx` (no real network calls). | ||
| - Raise typed errors from `errors.py` (subclasses of `BaseAuthError` with `get_status_code()` / `get_error_code()`). Wrap third-party failures in a typed error with `raise ... from e`. Inside a catch-all, re-raise this SDK's own errors first (`except VerifyAccessTokenError: raise`, as `verify_access_token` does) so they are not double-wrapped. Never return `None` or a bare `dict` to signal failure, and never leak raw `httpx`/`authlib` errors from public methods. |
There was a problem hiding this comment.
This says to never let raw httpx errors out of public methods, but the next bullet says a cold-cache discovery outage does exactly that in all three token methods, and the README notes it for the cases where verification is skipped. The 'Fail open' line further down reads the same way.
Shall we say 'in new code' here so it doesn't read as already true everywhere?
| - Follow existing style and naming. Add new public `ApiClient` methods after the existing public methods, above the `# ===== Private Methods =====` banner in `api_client.py`. Add new private helpers below it. Never reorder existing declarations. | ||
| - Add unit tests for new functionality, marked `@pytest.mark.asyncio` when async, mocking HTTP with `pytest-httpx` (no real network calls). | ||
| - Raise typed errors from `errors.py` (subclasses of `BaseAuthError` with `get_status_code()` / `get_error_code()`). Wrap third-party failures in a typed error with `raise ... from e`. Inside a catch-all, re-raise this SDK's own errors first (`except VerifyAccessTokenError: raise`, as `verify_access_token` does) so they are not double-wrapped. Never return `None` or a bare `dict` to signal failure, and never leak raw `httpx`/`authlib` errors from public methods. | ||
| - Route discovery and JWKS fetches through `ApiClient._discover` / `_fetch_jwks`, which call `fetch_oidc_metadata` / `fetch_jwks`, so both `custom_fetch` and caching apply. Token endpoint calls use `httpx.AsyncClient(timeout=httpx.Timeout(self.options.timeout))`, map `httpx.TimeoutException` to `ApiError` 504 and `httpx.HTTPError` to 502, and reject a negative `expires_in` with `ApiError("invalid_response", ..., 502)`. Copy `get_token_by_exchange_profile` for error-body and invalid-JSON handling, since it guards the error parse and uses 502. `get_access_token_for_connection` calls `response.json()` on the error path unguarded and raises `invalid_json` with the default status 500. In all three token methods `_discover()` runs before the `try`, so a discovery outage with a cold cache raises a raw `httpx` error instead of an `ApiError`. A discovery or JWKS outage during verification (connection error, timeout, HTTP 5xx or 429, classified by `is_auth_server_outage`) raises `AuthServerUnavailableError` (503), not a 401. |
There was a problem hiding this comment.
Since #153, every request the default HTTP client sends passes headers=self._telemetry_headers (discovery, JWKS and the three token endpoint calls). This bullet describes how to write a token endpoint call but doesn't mention it, so a new call written from this guidance would go out without the Auth0-Client and User-Agent headers.
Shall we add it here?
| - Follow existing style and naming. Add new public `ApiClient` methods after the existing public methods, above the `# ===== Private Methods =====` banner in `api_client.py`. Add new private helpers below it. Never reorder existing declarations. | ||
| - Add unit tests for new functionality, marked `@pytest.mark.asyncio` when async, mocking HTTP with `pytest-httpx` (no real network calls). | ||
| - Raise typed errors from `errors.py` (subclasses of `BaseAuthError` with `get_status_code()` / `get_error_code()`). Wrap third-party failures in a typed error with `raise ... from e`. Inside a catch-all, re-raise this SDK's own errors first (`except VerifyAccessTokenError: raise`, as `verify_access_token` does) so they are not double-wrapped. Never return `None` or a bare `dict` to signal failure, and never leak raw `httpx`/`authlib` errors from public methods. | ||
| - Route discovery and JWKS fetches through `ApiClient._discover` / `_fetch_jwks`, which call `fetch_oidc_metadata` / `fetch_jwks`, so both `custom_fetch` and caching apply. Token endpoint calls use `httpx.AsyncClient(timeout=httpx.Timeout(self.options.timeout))`, map `httpx.TimeoutException` to `ApiError` 504 and `httpx.HTTPError` to 502, and reject a negative `expires_in` with `ApiError("invalid_response", ..., 502)`. Copy `get_token_by_exchange_profile` for error-body and invalid-JSON handling, since it guards the error parse and uses 502. `get_access_token_for_connection` calls `response.json()` on the error path unguarded and raises `invalid_json` with the default status 500. In all three token methods `_discover()` runs before the `try`, so a discovery outage with a cold cache raises a raw `httpx` error instead of an `ApiError`. A discovery or JWKS outage during verification (connection error, timeout, HTTP 5xx or 429, classified by `is_auth_server_outage`) raises `AuthServerUnavailableError` (503), not a 401. |
There was a problem hiding this comment.
This bullet is around 125 words and mixes discovery routing, token endpoint calls, error body handling and outage mapping.
Shall we split it into sub-bullets, like the token store section below?
|
|
||
| ## Releases | ||
|
|
||
| `.version` is the version source. `.shiprc` keeps `pyproject.toml` in step with it. Releases are cut by `publish.yml` (manual dispatch, gated on the RL scan), not by an agent editing version files. Nothing in `.github/workflows/`, `.github/actions/`, or `.shiprc` writes `CHANGELOG.md`. Release PRs change it together with the version files. |
There was a problem hiding this comment.
publish.yml also creates the GitHub release, so 'cut by publish.yml' covers more than the publish step. It might be worth a half sentence, since this is where an agent would look to see what a release touches.
| # Common Pitfalls | ||
|
|
||
| 1. **Do not run `ruff format .`.** CI runs only `ruff check .`. The codebase is not ruff-formatted, so a repo-wide format produces a large unrelated diff. Match surrounding formatting by hand. | ||
| 2. **Python floor compatibility.** The floor is the `python` constraint under `[tool.poetry.dependencies]` in `pyproject.toml`. `.ruff.toml` `target-version` and the `test.yml` matrix hard-code their own versions and do not follow it, so check all three agree, and raise a mismatch rather than fixing CI or ruff config yourself (Ask First). The CI matrix pins the minor version only, so it tests the latest patch release, not the exact floor patch. Avoid syntax newer than the floor. While it is below 3.10, that means no `X | Y` type unions at runtime and no `match` statements. `ada-url` has separate version constraints above and below 3.10 in `pyproject.toml`. |
There was a problem hiding this comment.
This item is around 100 words and covers the floor definition, the three places that carry the version, and the 3.10 syntax limits.
Shall we split it into sub-points?
| 1. **Do not run `ruff format .`.** CI runs only `ruff check .`. The codebase is not ruff-formatted, so a repo-wide format produces a large unrelated diff. Match surrounding formatting by hand. | ||
| 2. **Python floor compatibility.** The floor is the `python` constraint under `[tool.poetry.dependencies]` in `pyproject.toml`. `.ruff.toml` `target-version` and the `test.yml` matrix hard-code their own versions and do not follow it, so check all three agree, and raise a mismatch rather than fixing CI or ruff config yourself (Ask First). The CI matrix pins the minor version only, so it tests the latest patch release, not the exact floor patch. Avoid syntax newer than the floor. While it is below 3.10, that means no `X | Y` type unions at runtime and no `match` statements. `ada-url` has separate version constraints above and below 3.10 in `pyproject.toml`. | ||
| 3. **Async tests need `@pytest.mark.asyncio`.** No `asyncio_mode` is set in `pyproject.toml`, so pytest-asyncio runs in strict mode and ignores an unmarked async test. Depending on the pytest version it then fails with an "async def functions are not natively supported" message or is skipped with a warning. If a new async test fails that way or shows as skipped, check the marker first. | ||
| 4. **Two dependency lists.** `pyproject.toml` + `poetry.lock` drive installs. `requirements.txt` is a hand-maintained partial list (`authlib`, `httpx`, `ada-url`, and the pytest packages, without `cryptography`, `jwcrypto`, `requests` or `ruff`) that only the Snyk SCA workflow reads. Changing one without the other leaves the scan out of date. |
There was a problem hiding this comment.
requirements.txt also lists freezegun, so 'the pytest packages' leaves one out of what the file covers.
| 2. **Python floor compatibility.** The floor is the `python` constraint under `[tool.poetry.dependencies]` in `pyproject.toml`. `.ruff.toml` `target-version` and the `test.yml` matrix hard-code their own versions and do not follow it, so check all three agree, and raise a mismatch rather than fixing CI or ruff config yourself (Ask First). The CI matrix pins the minor version only, so it tests the latest patch release, not the exact floor patch. Avoid syntax newer than the floor. While it is below 3.10, that means no `X | Y` type unions at runtime and no `match` statements. `ada-url` has separate version constraints above and below 3.10 in `pyproject.toml`. | ||
| 3. **Async tests need `@pytest.mark.asyncio`.** No `asyncio_mode` is set in `pyproject.toml`, so pytest-asyncio runs in strict mode and ignores an unmarked async test. Depending on the pytest version it then fails with an "async def functions are not natively supported" message or is skipped with a warning. If a new async test fails that way or shows as skipped, check the marker first. | ||
| 4. **Two dependency lists.** `pyproject.toml` + `poetry.lock` drive installs. `requirements.txt` is a hand-maintained partial list (`authlib`, `httpx`, `ada-url`, and the pytest packages, without `cryptography`, `jwcrypto`, `requests` or `ruff`) that only the Snyk SCA workflow reads. Changing one without the other leaves the scan out of date. | ||
| 5. **`custom_fetch` covers only discovery and JWKS.** `fetch_oidc_metadata` and `fetch_jwks` (`utils.py`) use the integrator's `custom_fetch` when set. Otherwise they create a plain `httpx.AsyncClient()` with no timeout, so `options.timeout` does not apply to them. The token endpoint calls in `get_access_token_for_connection`, `get_token_by_exchange_profile`, and `get_client_credentials_token` always build their own `httpx.AsyncClient` with `options.timeout`. Do not assume a new HTTP path inherits either behaviour, pick the matching pattern deliberately. |
There was a problem hiding this comment.
This says the plain httpx.AsyncClient() has no timeout, but httpx 0.28.1 (the version in poetry.lock) applies a 5 second default when none is given. What doesn't apply to these two fetches is options.timeout. The 'no timeout' wording came from a comment I left on CLAUDE.md earlier, and it was wrong.
Shall we say they use the httpx default of 5 seconds and that options.timeout doesn't apply to them?
| 2. **Python floor compatibility.** The floor is the `python` constraint under `[tool.poetry.dependencies]` in `pyproject.toml`. `.ruff.toml` `target-version` and the `test.yml` matrix hard-code their own versions and do not follow it, so check all three agree, and raise a mismatch rather than fixing CI or ruff config yourself (Ask First). The CI matrix pins the minor version only, so it tests the latest patch release, not the exact floor patch. Avoid syntax newer than the floor. While it is below 3.10, that means no `X | Y` type unions at runtime and no `match` statements. `ada-url` has separate version constraints above and below 3.10 in `pyproject.toml`. | ||
| 3. **Async tests need `@pytest.mark.asyncio`.** No `asyncio_mode` is set in `pyproject.toml`, so pytest-asyncio runs in strict mode and ignores an unmarked async test. Depending on the pytest version it then fails with an "async def functions are not natively supported" message or is skipped with a warning. If a new async test fails that way or shows as skipped, check the marker first. | ||
| 4. **Two dependency lists.** `pyproject.toml` + `poetry.lock` drive installs. `requirements.txt` is a hand-maintained partial list (`authlib`, `httpx`, `ada-url`, and the pytest packages, without `cryptography`, `jwcrypto`, `requests` or `ruff`) that only the Snyk SCA workflow reads. Changing one without the other leaves the scan out of date. | ||
| 5. **`custom_fetch` covers only discovery and JWKS.** `fetch_oidc_metadata` and `fetch_jwks` (`utils.py`) use the integrator's `custom_fetch` when set. Otherwise they create a plain `httpx.AsyncClient()` with no timeout, so `options.timeout` does not apply to them. The token endpoint calls in `get_access_token_for_connection`, `get_token_by_exchange_profile`, and `get_client_credentials_token` always build their own `httpx.AsyncClient` with `options.timeout`. Do not assume a new HTTP path inherits either behaviour, pick the matching pattern deliberately. |
There was a problem hiding this comment.
The README (section 10) says requests served by custom_fetch don't carry the telemetry headers either, which this pitfall could mention alongside options.timeout.
Also, 'Do not assume a new HTTP path inherits either behaviour, pick the matching pattern deliberately' joins two clauses with a comma.
Summary
Adds agent guidelines for this SDK: a
CLAUDE.mdwith project structure, boundaries, and security considerations, anAGENTS.mdthat points to it, and areferences/folder with the detail behind each section. No library code changes.Changes
CLAUDE.mdcovering the role, working principles, project structure, Always Do / Ask First / Never Do boundaries, and security considerations (algorithm pinning, token-derived hosts, unverified JWT values, organization policy scope, DPoP, token exchange, token store caching and cache keys,verified=trust boundary, error handling).AGENTS.md, which referencesCLAUDE.md.references/files, read on demand fromCLAUDE.md:commands.md: CI-matching install, test, and lint commandstesting.md: fixtures, async test patterns, coveragecode-style.md: naming, error patterns, comment rulesflow-map.md: entry points, helpers, tests, and docs per flowdocs-update.md: tracked docs and a code-to-docs mappinggit-workflow.md: branches, commits, PR template, CI workflowspitfalls.md: common failuresTesting
No source, tests, or dependencies changed, so there is no runtime or test impact.