Skip to content

Tooling for custom runestone builds - #1643

Open
ascholerChemeketa wants to merge 3 commits into
RunestoneInteractive:mainfrom
ascholerChemeketa:custom-activecode
Open

ascholerChemeketa wants to merge 3 commits into
RunestoneInteractive:mainfrom
ascholerChemeketa:custom-activecode

Conversation

@ascholerChemeketa

Copy link
Copy Markdown
Contributor

Handle iframe resizing and add a proxy service for custom builds of runestone running in an interactive's iframe.


This pull request introduces a new allowlisted experiment API tunnel to the book server and adds support for automatic iframe resizing for experimental Runestone components. It also includes comprehensive tests for the new API tunnel.

Experiment API Tunnel

  • Added an allowlisted experiment API tunnel endpoint (/experiments/{service}/{path}) to rsproxy.py, with strict SSRF protections, response size limits, timeout handling, and error mapping. Only explicitly named APIs (currently rdaas/api/duck) are accessible, and responses are capped in size and not cached. [1] [2] [3]
  • Added tests to verify correct behavior of the experiment API tunnel, including allowlist enforcement, error relaying, redirect refusal, response size capping, and timeout handling.

Frontend: Iframe Resize Support

  • Added pretext-iframe-resize.js, a script that enables experimental Runestone components in iframes to automatically request resizing from the parent PreTeXt page when their content changes.
  • Registered the new resize script in the main interactive webpack entry (webpack.index.js) so it is loaded with other common JS assets.

Copilot AI balanced review requested due to automatic review settings October 30, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pooled upstream client can leak session cookies between students, and the new tests fail to exercise the streaming size-limit guard.

2 open issues
What changed in this PR

Adds support for custom Runestone components embedded in PreTeXt iframes, including automatic resizing and a book-server tunnel to an allowlisted experiment API.

Changes:

  • Adds an experiment API endpoint with redirect refusal, response-size limits, and timeout/error handling.
  • Reports iframe content-height changes to the parent page.
  • Adds proxy tests for allowlist enforcement, credential omission, and upstream failure handling.
File Description
test/​bases/​rsptx/​book_server_api/​test_rsproxy.py Tests experiment proxy behavior and safeguards.
bases/​rsptx/​interactives/​webpack.index.js Includes the iframe resize script in the interactive bundle.
bases/​rsptx/​interactives/​runestone/​common/​js/​pretext-iframe-resize.js Observes wrapper size and sends height updates to the parent.
bases/​rsptx/​book_server_api/​routers/​rsproxy.py Implements the allowlisted experiment API tunnel.

Review effort: Balanced

Comment on lines +289 to +294
async def test_experiment_api_caps_response(monkeypatch):
monkeypatch.setattr(rsproxy, "MAX_EXPERIMENT_RESPONSE_BYTES", 10)
install_mock("experiment", lambda request: httpx.Response(200, content=b"x" * 11))
async with book_client() as client:
resp = await client.get("/rsproxy/experiments/rdaas/api/duck")
assert resp.status_code == 502
@bnmnetp

bnmnetp commented Nov 2, 2026

Copy link
Copy Markdown
Member

@aadarsh-padiyath @sethbern -- when I merge this in a day or so, this is the basis for your work on the explain in plain English. @ascholerChemeketa has a repository with a sample project at https://github.com/ascholerChemeketa/RunestoneResearch to illustrate how to use your own custom bundle of the components along with the PreTeXt interactive tag.

Added a strip_cookies function to remove cookies from requests before sending them to the experiment API.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings November 2, 2026 16:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The proxy serves arbitrary upstream HTML under the book server’s origin, allowing upstream JavaScript to execute with same-origin access.

3 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Use the parent origin for cross-origin iframe resize messages

bases/​rsptx/​interactives/​runestone/​common/​js/​pretext-iframe-resize.js:34

Using the iframe's own origin as targetOrigin prevents resizing when a custom build is hosted on a different origin from the book page: the browser checks this against the receiving parent, so every height report is discarded. PreTeXt's lti_iframe_resizer.js already supports cross-origin senders through contentWindow matching. Send to a configured parent origin, or use "*" for this non-sensitive, height-only message.

Medium severity Test the streaming size limit without Content-Length

test/​bases/​rsptx/​book_server_api/​test_rsproxy.py:291

This mock uses content=, so HTTPX supplies Content-Length and the endpoint returns before exercising the streaming size limit. The test would still pass if that limit were removed, leaving chunked responses without a declared length unprotected against that regression. Keep the declared-length case and add an httpx.AsyncByteStream response whose chunks individually fit but together exceed the limit.

1 resolved since last review

Review effort: Balanced

Comment on lines +243 to +261
async def test_experiment_api_fetches_only_allowlisted_target():
def handler(request):
assert str(request.url) == "https://rdaas.flypages.dev/api/duck?color=green"
assert "cookie" not in request.headers
assert "authorization" not in request.headers
assert "x-api-key" not in request.headers
return httpx.Response(
200, json={"duck": "quack"}, headers={"Content-Type": "application/json"}
)

install_mock("experiment", handler)
async with book_client() as client:
resp = await client.get(
"/rsproxy/experiments/rdaas/api/duck?color=green",
headers={"Cookie": "student_session=private", "X-API-KEY": "private"},
)
assert resp.status_code == 200
assert resp.json() == {"duck": "quack"}
assert resp.headers["cache-control"] == "no-store"
@ascholerChemeketa

Copy link
Copy Markdown
Contributor Author

Although it creates risks if a researcher is using some API they don't control, I think we don't want to restrict what an experiment author can do with retrieved content. What do you think @bnmnetp ?

@bnmnetp

bnmnetp commented Nov 2, 2026

Copy link
Copy Markdown
Member

I think we need to trust the experimenter the same way we trust an author. Usually there are almost one and the same. Or certainly working closely together.

If there is a plausible scenario where this can be used to attack, then we should pay attention.

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.

3 participants