Skip to content

Add an opt-in allowlist of iframe embed providers, on by default for new installs - #480

Merged
jeremy merged 8 commits into
mainfrom
iframe-provider-allowlist
Sep 10, 2026
Merged

Add an opt-in allowlist of iframe embed providers, on by default for new installs#480
jeremy merged 8 commits into
mainfrom
iframe-provider-allowlist

Conversation

@jeremy

@jeremy jeremy commented Aug 26, 2026

Copy link
Copy Markdown
Member

What & why

Authored book content is rendered to unauthenticated readers, and HtmlScrubber keeps an <iframe src=…> from any origin. That is wider than authoring needs (a video or a map embed) and means every reader's session can load an arbitrary third-party frame chosen by the author.

This adds an opt-in allowlist of embed providers. Nothing changes for an existing install until it opts in; a fresh install starts on a curated list.

Behavior by install

Install Default How it gets there
Existing install, upgraded Permissive — exactly as today. Any origin, attributes scrubbed as on main, no Content-Security-Policy. The migration adds accounts.embed_providers as NULL; nothing reads a NULL row as a policy.
New install Curated allowlist: YouTube, Vimeo, Loom, Google Maps. FirstRun.create! seeds EmbedProvider::DEFAULTS into the account row (app/models/first_run.rb) — the one seam where a fresh install initializes state.
Any install, opted in Allowlist enforced. Set WRITEBOOK_EMBED_PROVIDERS (env) or Account#embed_providers (DB).

Precedence

WRITEBOOK_EMBED_PROVIDERS overrides Account#embed_providers overrides permissive. Whichever source applies is the whole table — the env var does not extend the account's list, it replaces it. Tested in EmbedProviderTest (env-only, account-only, env-over-account, neither, no account row) and ContentSecurityPolicyTest.

Opting in on an existing install

  • Environment: WRITEBOOK_EMBED_PROVIDERS='[…]' — a JSON array of provider entries (a single object is also accepted). EmbedProvider::DEFAULTS is in this exact form, so the curated list is bin/rails runner 'puts EmbedProvider::DEFAULTS.to_json'.
  • Account setting: bin/rails runner 'Account.first.update!(embed_providers: EmbedProvider::DEFAULTS)'. There is no admin UI for it in this PR; the setting exists so a new install has somewhere to start, and it is where a future settings page would write.

Invalid JSON in the env var is logged and ignored (the account setting still applies). A configured table whose entries are all invalid is configured-but-empty: every iframe is stripped and frame-src 'none' is sent — it does not fall through to a wider source. [] is therefore also the way to disable iframes entirely.

Once configured: two enforcement points, one table

app/models/embed_provider.rb is the one table both legs read: { name, hosts, path_prefix, attributes }.

  • Author-time (scrubber). HtmlScrubber keeps an <iframe> only when its src matches a provider's host and path shape, and strips every attribute the provider doesn't permit. Anything else is stripped.
  • Render-time (CSP). ApplicationController sets a frame-src directive derived from the same table (content_security_policy if: -> { EmbedProvider.configured? }). It lives on the controller rather than the initializer so that no directive is sent while permissive — a frame-src with an empty source list would block every frame. Only frame-src is set; the rest of the policy is left unrestricted.

A src is accepted only when it is https, on the default port, with an exact host (case-insensitive), and a segment-boundary path match. A valid host with the wrong path (youtube.com/watch?v=…), a lookalike host, userinfo confusion (youtube.com@evil.com), protocol-relative / data: / http: URLs, an explicit non-443 port, and dot-segment or encoded traversal past the prefix are all rejected.

Provider entries

[
  { "name": "Wistia", "hosts": ["fast.wistia.net"], "path_prefix": "/embed/" }
]
  • hosts (required) — exact DNS hostnames (string or array). Wildcards, whitespace, and IP literals are rejected.
  • path_prefix (required) — permitted path, matched on a segment boundary. Canonicalized before use (duplicate slashes collapsed, trailing slash dropped); an entry whose prefix contains a ./.. segment or reduces to / (/., /./, //) is dropped, since a browser resolves those to the root.
  • attributes (optional) — iframe attributes to retain; defaults to src width height allowfullscreen frameborder title loading. Always intersected with that master list, so config can never reintroduce srcdoc, sandbox, name, on*, style, allow, or referrerpolicy.

Matching is first-match-wins across the table.

Fragment caches

The leaves/_leaf and books/show fragments wrap scrubbed content, so their keys include HtmlScrubber.cache_version — a scrubber policy version plus EmbedProvider.cache_version, which is "permissive" or a digest of the effective table in resolution order. Configuring an allowlist over a permissive install, editing the table, or tightening the scrubber re-renders through the scrubber instead of serving a fragment cached under the old policy. Tested both ways in BooksControllerTest with fragment caching on.

Tests

  • Permissive default: an iframe from any origin survives with attributes scrubbed as on main, and no CSP header is sent (HtmlScrubberTest, PagesHelperTest, PagesControllerTest, ContentSecurityPolicyTest).
  • Opt-in via env, via the account setting, precedence, no-account-row, and the curated defaults round-tripping through the env format (EmbedProviderTest).
  • New-install seed and the upgraded-install null row (FirstRunTest).
  • Each default provider's valid embed passes; disallowed origins, wrong path shapes, and the bypass catalogue are rejected; forbidden attributes are stripped from an approved iframe; config validation fails closed.
  • CSP frame-src reflects exactly the configured table, an env table replaces the account's without a restart, and an all-invalid table emits 'none'.
  • Fragment cache re-scrubs on permissive → allowlist and on a narrowed allowlist.

Full suite, RuboCop, and Brakeman are green.

Copilot AI balanced review requested due to automatic review settings August 26, 2026 04:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Restricts iframe embeds to approved providers using shared scrubber and CSP configuration.

Changes:

  • Adds configurable provider, host, path, and attribute allowlists.
  • Enforces restrictions during sanitization and through CSP.
  • Adds model, helper, controller, and integration coverage.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
app/models/embed_provider.rb Defines providers and matching rules.
app/models/html_scrubber.rb Filters iframes and attributes.
config/initializers/content_security_policy.rb Generates frame-src policy.
test/models/embed_provider_test.rb Tests provider matching and configuration.
test/helpers/pages_helper_test.rb Tests iframe sanitization.
test/controllers/pages_controller_test.rb Tests rendered page behavior.
test/integration/content_security_policy_test.rb Tests CSP response headers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/models/embed_provider.rb
Comment thread app/models/embed_provider.rb
Comment thread test/integration/content_security_policy_test.rb Outdated
Comment thread app/models/embed_provider.rb Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd44482e5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/models/html_scrubber.rb
Comment thread app/models/embed_provider.rb
Comment thread test/integration/content_security_policy_test.rb Outdated
Comment thread app/models/embed_provider.rb
@jeremy
jeremy force-pushed the iframe-provider-allowlist branch 2 times, most recently from 4037c10 to ba8981f Compare August 26, 2026 05:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba8981fb08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread config/initializers/content_security_policy.rb Outdated
Authored book content is rendered to unauthenticated readers, but the
HtmlScrubber allowed <iframe src=…> from any origin. Narrow that to an
allowlist of approved embed providers, enforced at both author time (the
scrubber) and render time (the CSP frame-src directive).

Both enforcement points read from a single provider table (EmbedProvider)
so they can't drift:

  * scrubber — keeps an <iframe> only when its src matches a provider's
    host and path shape, and strips every attribute the provider doesn't
    permit (no srcdoc, sandbox, name, or on* handlers ride along).
  * CSP — frame-src is derived from the same table.

Ships with YouTube, Vimeo, Loom, and Google Maps enabled by default, each
pinned to its embed path shape. Self-hosted operators extend the table per
install via the WRITEBOOK_EMBED_PROVIDERS environment variable (JSON),
which widens the scrubber allowance and the CSP directive together. There
is no raw-iframe escape hatch: an embed is permitted only via a vetted
provider entry.
@rosa
rosa force-pushed the iframe-provider-allowlist branch from ba8981f to ff2c5bd Compare September 9, 2026 07:44
@rosa

rosa commented Sep 9, 2026

Copy link
Copy Markdown
Member

🤖 Rebased onto main @ 3f98703 and force-pushed ff2c5bd.

Conflict: app/models/html_scrubber.rb, against #485 (explicit attribute allowlist + data-action gating). Kept both — keep_node?/scrub_attributes compose with main's scrub + remove_foreign_actions, and the iframe branch filters against the provider's attribute list rather than self.attributes, which is the tighter of the two. One test from main needed repointing: "strips value-sensitive iframe attributes but keeps safe embed attributes" used src="https://ex.com", now off-allowlist, so the iframe is removed entirely and the "keeps" half had nothing left to assert on. It uses a Vimeo embed now; the attribute policy it tests is unchanged.

Changes:

  • new_session_urlnew_session_path (AGENTS.md wants _path in integration tests) — a still-open review comment.
  • Added an integration test that an operator-configured provider reaches the real Content-Security-Policy header without a restart. The existing coverage only proved the header equals csp_frame_sources; this proves the per-request lambda is what keeps them equal.

Verified by hand (bin/rails runner probes against EmbedProvider and the scrubber): backslash in the authority, tab/newline in the host, userinfo, IPv6 literal, punycode, // path, /./, relative, scheme-relative, data:, javascript: and http: are all rejected, and everything accepted is genuinely on a provider host (:0443 normalizes to 443 in Ruby and in the URL spec alike; fragments and query strings don't move the path; %252e stays literal in a browser too). Duplicate SRC=, uppercase attributes, xlink:/xml:-prefixed attributes, srcdoc, data-controller/data-action and svg/math foreign contexts all end up as <iframe src="…youtube…"> or nothing. No drift in the dangerous direction: every kept src has an exact host in the table and csp_sources emits https://<host> for it. The app renders no iframes of its own, so a frame-src without 'self' breaks nothing.

On the open thread at content_security_policy.rb:17 (a proc returning an array serializing as ["https://…"]): that isn't true on this Rails — resolve_source does apply_mappings(Array.wrap(resolved)), and ContentSecurityPolicyTest now asserts the real header tokens. The eight older inline comments were against cd44482e and are already handled on the branch (dot segments, allow/referrerpolicy, host validation, style, non-default ports).

Tests: bin/rails test 253 runs / 919 assertions / 0 failures · bin/rubocop clean · bin/brakeman 0 warnings.

Review: no independent adversarial round on this push — the review agent pool was saturated. The 2026-08-26 Codex review from the GitHub connector is the only outside review on record.

Still a product call. The card is tagged PRODUCT DECISION and this PR doesn't settle it: the default provider set, and what happens to already-published books whose embeds this strips. There's no audit, no author warning and no grandfather window here — an off-allowlist embed disappears on the next render, and the iframe's fallback text becomes visible body text.

The leaf and book fragments wrap scrubbed page content, so a fragment cached
before a policy change keeps serving markup the scrubber would now strip:
an iframe from a since-removed provider, or attributes the provider no
longer permits. CSP allows the host, so nothing downstream catches it.

Key both fragments on HtmlScrubber.cache_version — a scrubber policy version
plus a digest of the effective provider table (hosts, path prefix,
attributes) — so a WRITEBOOK_EMBED_PROVIDERS edit or a scrubber tightening
re-renders through the scrubber instead of hitting the stale fragment.
A configured path_prefix of "/.", "/./" or "//" passed validation (it starts
with a slash and is longer than one character) but a browser resolves each
to "/", so the entry became a whole-host allowance rather than the path
shape the operator wrote.

Canonicalize the prefix before accepting it: collapse duplicate slashes,
drop the trailing slash, and drop the entry when any segment is "." or
".." or nothing but the root remains. The canonical form is what gets
stored and matched against.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T22:42:02.758227Z e89c1d4 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Pushed two fixes from the adversarial review, as separate commits:

Stale fragment caches38d1df2. leaves/_leaf and books/show wrap scrubbed page content in cache blocks keyed only on the records, so a fragment cached before this deploy (or before a WRITEBOOK_EMBED_PROVIDERS change) kept serving the pre-policy iframe markup: the cache hit skips the scrubber and the CSP permits the host. Both keys now include HtmlScrubber.cache_version — a scrubber POLICY_VERSION plus EmbedProvider.cache_version, a digest of the effective provider table (hosts, path prefix, attributes; order-insensitive). The other two cache blocks (books/index, books/_book) render no scrubbed content and are unchanged. Test: BooksControllerTest turns on fragment caching with a memory store, renders a book under a policy that permits x.example (asserting a fragment write actually happened), narrows the policy, and asserts the re-render is scrubbed. Red without the key change; also red with only the leaf key changed, since the outer books/show fragment contains the rendered leaves.

Root-equivalent path prefixesbde9da8. valid_path_prefix? accepted /., /./ and //, which a browser resolves to /, turning the entry into a whole-host allowance. Config prefixes are now canonicalized before acceptance: duplicate slashes collapsed, trailing slash dropped, and the entry is dropped (logged) when any segment is . or .. or only the root remains. The canonical form is what's stored and matched. /embed//x/ canonicalizes to /embed/x — collapsing rather than rejecting, because that's the path the origin server sees. Tests cover /, /., /./, //, /embed/.., /embed/../, /embed/./.., a missing leading slash, and the canonical-form case.

bin/rails test 259 runs / 968 assertions / 0 failures (1 pre-existing libvips skip) · bin/rubocop clean · bin/brakeman 0 warnings.

The nine open inline threads are all against cd44482e/ba8981fb and were addressed by the rebase push (ff2c5bd); I'll reply on each with where it landed and resolve. I'm babysitting the review loop on this PR and will drive it to convergence.

@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bde9da869a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/controllers/pages_controller_test.rb Outdated
Comment thread test/models/embed_provider_test.rb Outdated
Comment thread app/models/embed_provider.rb Outdated
Matching is first-match-wins, so two entries overlapping on host and path
but differing in attributes are a different policy in either order. Sorting
the signatures hid that, so a reordered config kept serving fragments
scrubbed under the previous order.
Clear WRITEBOOK_EMBED_PROVIDERS for every test and restore it afterwards,
so a value in the operator's shell neither fails the default-table
assertions nor gets dropped for the rest of the run. Reuse the welcome
page fixture in the iframe show tests.
@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 667878ee82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The security-sensitive URL validation, sanitization, CSP, and persistent-cache interactions warrant final human review.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Review loop converged at 667878e: Codex found no issues on the head (its round on bde9da8 raised three threads — cache digest in resolution order, hermetic WRITEBOOK_EMBED_PROVIDERS handling in tests, fixture reuse — all applied in fea5c0f/667878e), Copilot reviewed the head with zero new comments, and all 12 threads are resolved with an hour of silence since. Suite 259 runs / 970 assertions / 0 failures, RuboCop clean, Brakeman 0 warnings. Ready for human review; not merging.

…rated list

Enforcing an allowlist by default would strip embeds that existing books
render today. Instead, embeds stay as they are — any origin, attributes
scrubbed, no Content-Security-Policy — until a provider table is
configured, and then the scrubber and frame-src are both derived from it.

The table comes from WRITEBOOK_EMBED_PROVIDERS first, then the account's
embed_providers setting, else nothing. FirstRun seeds the curated defaults
into the account, so a fresh install starts on YouTube, Vimeo, Loom and
Google Maps, while an install upgraded from before the setting keeps a
null row and stays permissive. An existing install opts in by setting the
environment variable or the account setting.

frame-src moves from the initializer to ApplicationController so that no
directive at all is sent while permissive; an empty frame-src would block
every frame. The fragment cache version names the permissive mode, so
configuring an allowlist re-renders cached pages through the scrubber.
@jeremy jeremy changed the title Restrict iframe embeds to an allowlist of approved providers (author scrubber + CSP frame-src) Add an opt-in allowlist of iframe embed providers, on by default for new installs Sep 9, 2026
@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Reworked per the product decision on the card: the allowlist is now opt-in, and only new installs start on the curated list — 9c47640.

  • Existing installs keep working as on main. With nothing configured, HtmlScrubber keeps an <iframe> from any origin with the same attribute scrubbing main does, and no Content-Security-Policy is sent. main's "show with iframes" test (an http://example.com frame surviving) is back and passes.
  • New installs get the curated defaults. The seam is FirstRun.create! (app/models/first_run.rb), the only place a fresh install initializes state: it seeds EmbedProvider::DEFAULTS into a new accounts.embed_providers column (db/migrate/20260909222120_add_embed_providers_to_accounts.rb, JSON-serialized in app/models/account.rb). An install upgraded from before the column gets NULL and stays permissive — an env var alone can't tell a new install from an existing one, which is why the setting is DB-backed.
  • PrecedenceWRITEBOOK_EMBED_PROVIDERS overrides Account#embed_providers overrides permissive, each source the whole table. Tested for every arm in EmbedProviderTest and ContentSecurityPolicyTest.
  • CSP moved from the initializer (restored to main's commented-out file) to ApplicationController's content_security_policy if: -> { EmbedProvider.configured? }, so that while permissive no directive is sent at all — a frame-src with an empty source list would block every frame, and the initializer form can't omit the directive per request.
  • Cache version names the permissive mode ("permissive" vs a digest of the table), so configuring an allowlist re-renders cached pages through the scrubber. BooksControllerTest covers permissive → allowlist and a narrowed allowlist; both go red if cache_version is held constant.
  • No admin UI: the only existing settings surface is the single-purpose Custom CSS page, so opting in is documented as the env var or a one-line bin/rails runner on the account (see the PR body).

bin/rails test 273 runs / 1026 assertions / 0 failures (1 pre-existing libvips skip) · bin/rubocop clean · bin/brakeman 0 warnings. PR title and body rewritten for the opt-in model. Babysitting the review loop to convergence.

@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

@jeremy
jeremy requested a balanced review from Copilot September 9, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Policy resolution is repeatedly recomputed per request and can amplify invalid configuration warnings into production log flooding.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread app/models/embed_provider.rb Outdated
The scrubber, the frame-src directive and the fragment cache key each read
the table several times per request, and every read re-validated the
entries and re-logged any rejected one. Keep the last resolution keyed by
its source entries, so a config typo is logged once and a changed
configuration is still picked up on the next read.
@jeremy
jeremy force-pushed the iframe-provider-allowlist branch from 987ee49 to 4c8c3db Compare September 9, 2026 22:31
@jeremy
jeremy requested a balanced review from Copilot September 9, 2026 22:31
@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Environment JSON is reparsed and invalid configuration is repeatedly logged on every policy lookup, risking excessive overhead and log volume.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread app/models/embed_provider.rb Outdated
The environment JSON was still parsed, and an unparsable value still
logged, on every read ahead of the memoized table. Key the resolution on
the raw environment value and the account row instead, so both the parse
and the validation happen once per distinct configuration.
@jeremy
jeremy requested a balanced review from Copilot September 9, 2026 22:37
@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Policy resolution repeatedly loads and deserializes the account configuration for every leaf cache key, causing N+1 work on uncached books.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread app/models/embed_provider.rb
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: e89c1d47a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy

jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Review loop converged at e89c1d4 for the opt-in rework. Codex found no issues on the head. Copilot reviewed each push: its first two findings (re-validating the table and re-logging per call; re-parsing the env value per call) are applied in 4c8c3db and e89c1d4; its third, the query-cached Account.first read per leaf on a cold fragment cache, is declined with the reasoning in the thread. 15/15 threads resolved, an hour of silence since. Suite 275 runs / 1033 assertions / 0 failures, RuboCop clean, Brakeman 0 warnings. Ready for human review; not merging.

@jeremy
jeremy merged commit 99ce38a into main Sep 10, 2026
8 checks passed
@jeremy
jeremy deleted the iframe-provider-allowlist branch September 10, 2026 06:37
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