Skip to content

reboot: bind an app_internal route's context to its endpoint - #171

Draft
rjhuijsman wants to merge 1 commit into
mainfrom
rjh.app-internal-context-per-endpoint
Draft

rjhuijsman wants to merge 1 commit into
mainfrom
rjh.app-internal-context-per-endpoint

Conversation

@rjhuijsman

Copy link
Copy Markdown
Contributor

Important

This PR was written by an AI agent and has not yet been reviewed by a human. It is awaiting the author's review before peer review.

Why

A custom HTTP route can opt into an app-internal ExternalContext (one carrying the application's caller_id, able to call app-internal-only servicers) by registering with app_internal=True. That grant must reach every request the route handles, whatever the shape of its path, and must not reach any other route.

Before this change, PythonWebFramework.HTTP._api_route recorded an app-internal route's path string in a set, and the external_context_middleware looked the request's path up in that set — before routing — to decide which context to put on request.state.reboot_external_context. A route whose path carries parameters (e.g. /__/things/{id}) therefore never matched its own requests: the set held the template while requests carried concrete paths, so such a route silently got the external context it had not asked for and failed later on the trusted call it was meant to make.

Pattern matching the paths in the middleware (Starlette's compile_path) would not have been a fix either: the middleware runs before routing and cannot know which endpoint Starlette will dispatch to, so a {param} pattern would also have granted the app-internal context to every ordinary route living under the same prefix (e.g. /__/things/health).

What

The grant is bound to the endpoint rather than to the path:

  • PythonWebFramework.APIRoute gains an app_internal: bool = False field; _api_route pops app_internal from the kwargs and stores it there. _app_internal_paths is gone.
  • When the server process registers routes (fastapi.add_api_route(...)), an app-internal route gets a FastAPI route-level dependency (dependencies=[Depends(...)]) that sets request.state.reboot_external_context to an app-internal context. A route-level dependency runs after Starlette has dispatched to that endpoint and before the endpoint itself, and it is prepended to any dependencies the route declared itself, so the endpoint's own InjectExternalContext already sees the app-internal context. The Depends object is created in the server process, so nothing new needs pickling.
  • The middleware now always installs the external context. Handlers keep reading request.state.reboot_external_context unchanged (reboot/mcp/context.py, the OAuth handlers), and the existing app_internal=True users (oauth_server.py, oauth_providers.py) need no change.

Tests

New tests/reboot/aio/http_app_internal_test.py (target //tests/reboot/aio:http_app_internal_test_py) brings an Application up with reboot.aio.tests.Reboot and hits three routes over HTTP; each handler reports whether the context it was handed carries a caller_id:

  1. /__/test/static registered with app_internal=True gets an app-internal context (regression check; already true before this change).
  2. /__/test/{item} registered with app_internal=True gets an app-internal context (failed before this change).
  3. /__/test/plain, under the same prefix but registered without app_internal, gets an external context (guards against the wildcard-leak approach).

Before (tests on top of unmodified main):

FAIL: test_parameterized_app_internal_route (__main__.HTTPAppInternalTest)
AssertionError: {'app_internal': True} != {'app_internal': False}
- {'app_internal': True}
+ {'app_internal': False}

Ran 3 tests in 7.553s

FAILED (failures=1)

After (this commit):

Ran 3 tests in 4.732s

OK
//tests/reboot/aio:http_app_internal_test_py PASSED in 9.0s
Executed 1 out of 1 test: 1 test passes.

mypy reboot/aio/http.py tests/reboot/aio/http_app_internal_test.py reports only the five pre-existing PyJWT bytes/str errors in oauth_server.py/tests.py.

The OAuth tests that exercise the existing app_internal=True routes (tests/reboot/aio/auth:oauth_providers_test_py and siblings) depend on //reboot/ping, which does not build from the mono workspace the change was tested in (a pre-existing pydantic_to_zod relative-import TypeError), so they are left to this PR's CI.

Relation to #109

This is orthogonal to and was split out of #109, which had briefly tried pattern-matching in the middleware, then a registration-time refusal of parameterized paths, and now leaves this to this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D8vHXG2KLHDUruv8642AXY

An HTTP route that opts into an app-internal `ExternalContext` via
`app_internal=True` must receive one for every request it handles,
whatever the shape of its path, and no other route may receive one.

Before this change, `PythonWebFramework.HTTP._api_route` recorded an
app-internal route's path string in a set, and the middleware that
puts the context on `request.state` looked the request's path up in
that set before routing. A route whose path carries parameters (e.g.
`/__/things/{id}`) therefore never matched its own requests — the set
held the template while requests carried concrete paths — so such a
route silently got the external context it had not asked for and
failed later on the trusted call it was meant to make. Pattern
matching the paths in the middleware would not have been a fix
either: the middleware runs before routing and cannot know which
endpoint Starlette will dispatch to, so a `{param}` pattern would have
granted the app-internal context to every ordinary route under the
same prefix as well.

The grant is now bound to the endpoint rather than to the path:
`APIRoute` carries an `app_internal` flag, and when the server process
registers such a route it attaches a FastAPI route-level dependency
that replaces the request's context with an app-internal one. That
dependency runs after Starlette has dispatched to the endpoint and
before the endpoint itself, so it reaches exactly that endpoint's
requests. The middleware now always installs the external context.
Handlers keep reading `request.state.reboot_external_context`, and
existing `app_internal=True` routes need no change.

The new `tests/reboot/aio/http_app_internal_test.py` covers a static
`app_internal=True` route, a parameterized one (which failed before
this change), and a route under the same prefix registered without
`app_internal` (which must keep its external context).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8vHXG2KLHDUruv8642AXY
@aviator-app

aviator-app Bot commented Sep 14, 2026

Copy link
Copy Markdown

Current Aviator status

Aviator will automatically update this comment as the status of the PR changes.
Comment /aviator refresh to force Aviator to re-examine your PR (or learn about other /aviator commands).

This pull request is currently open (not queued).

How to merge

To merge this PR, comment /aviator merge or add the mergequeue-ready label.


See the real-time status of this PR on the Aviator webapp.
Use the Aviator Chrome Extension to see the status of your PR within GitHub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant