Skip to content

fix(security): derive CSP origins on release-backed content - #3487

Merged
kojiwakayama merged 2 commits into
mainfrom
fix/derived-csp-release-context
Aug 8, 2026
Merged

fix(security): derive CSP origins on release-backed content#3487
kojiwakayama merged 2 commits into
mainfrom
fix/derived-csp-release-context

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The diagnostics from #3482 named this on the first request after deploy:

info  Derived CSP origins   contentVersion "environment:preview@…"        fileCount 2  originCount 2
warn  read no project sources  contentVersion "release:f6749fdf…@…"

Never once a success for a release: context — same for codersociety and tomcode. The failure is specific to release-backed content, not to derivation.

Cause 1 — the read never waits. getAllSourceFiles answers empty on a cache miss and schedules the fetch in the background. Correct for callers that can proceed without the list; fatal here, because nothing else populates it for a release-backed context, so the read was empty on every request for the life of the process. It now waits for the fetch it just started.

Made opt-in rather than changing it for everyone: waiting unconditionally also pulls CSS pregeneration into the request. A test (does not pregenerate CSS during branch cache warmup) caught that when I first did it the blunt way, so only CSP derivation opts in.

Cause 2 — the cache key contained [object Promise]. The multi-project wrapper's getSourceSnapshotVersion is async and was being template-stringified. Every content version of every project collapsed to one key, so a push to a preview would have served the previous derivation. Now awaited, and the test wrapper is async like the real one.

This is the third attempt. #3474 fixed real negative caching, #3484 fixed a real missing initialization, and I claimed each would close this out. Neither was the cause. What changed is where the answer came from: production logs, not me reasoning about which layer looked suspicious. That is what #3482 was for, and it worked.

Gate green by exit code: lint, lint:test-typecheck, typecheck, fmt, docs, full unit suite.

Verification is still one curl after promotion — vf-csp-probe.production.veryfront.com gaining images.unsplash.com and cdn.jsdelivr.net in img-src — and if it fails again the logs will say why without another guess.

Summary by CodeRabbit

  • Bug Fixes
    • Updated runtime security policy generation to detect source changes made within the same release, preventing stale cached results.
    • Improved source file loading during startup and refresh operations, including cases where caching is temporarily unavailable.
  • Improvements
    • Added an option to wait for source files to finish loading before they are read, providing more reliable project data and runtime configuration.

Production logs from #3482 named it on the first try. Preview derived from
"environment:preview" with fileCount 2 and originCount 2; production read no
sources at all from "release:<id>", every request, for every hosted project.
The failure was specific to release-backed content, not to derivation.

Two causes, both visible in that one log line.

`getAllSourceFiles` answers empty on a cache miss and schedules the fetch in
the background. That is right for callers who can proceed without the list, but
nothing else populates it for a release-backed context, so the read was empty
on every request for the life of the process. It now waits for the fetch it
just started -- opt-in, because making every caller wait also pulls CSS
pregeneration into the request, which a test correctly caught.

The cache key contained the literal "[object Promise]": the multi-project
wrapper's `getSourceSnapshotVersion` is async and was being
template-stringified. Every content version of every project collapsed to one
key, so a push to a preview would have served the previous derivation.

This is the third attempt at this bug. #3474 fixed real negative caching and
#3484 fixed a real missing initialization; neither was the cause. The
difference this time is that the answer came from production logs rather than
from me reasoning about which layer looked suspicious -- which is what #3482
was for.
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 8, 2026 21:20
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The adapters now return warmup file data and support optional warmup waits. Runtime CSP derivation awaits snapshot versions and waits for release-backed sources. Tests verify reloading when the snapshot version changes.

Changes

Source warmup and CSP derivation

Layer / File(s) Summary
Warmup result handling
src/platform/adapters/fs/veryfront/adapter.ts
File warmup returns fetched files or null. Read readiness uses a completion-only promise.
Warmup-aware source listing
src/platform/adapters/fs/veryfront/adapter.ts, src/platform/adapters/fs/veryfront/multi-project-adapter.ts
getAllSourceFiles accepts waitForWarmup and uses warmup results or a cache retry on misses.
Snapshot-aware CSP loading
src/server/runtime-handler/project-runtime-context.ts, src/server/runtime-handler/derive-project-csp.test.ts
CSP loading awaits snapshot versions, waits for source warmup, and tests reloading for changed snapshots.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProjectRuntimeContext
  participant MultiProjectFSAdapter
  participant VeryfrontFSAdapter
  participant FileListCache
  ProjectRuntimeContext->>ProjectRuntimeContext: await source snapshot version
  ProjectRuntimeContext->>MultiProjectFSAdapter: getAllSourceFiles({waitForWarmup: true})
  MultiProjectFSAdapter->>VeryfrontFSAdapter: forward warmup option
  VeryfrontFSAdapter->>FileListCache: fetch or retry source files
  FileListCache-->>VeryfrontFSAdapter: warmed source files
  VeryfrontFSAdapter-->>ProjectRuntimeContext: return source files
  ProjectRuntimeContext-->>ProjectRuntimeContext: derive CSP with snapshot-aware key
Loading

Possibly related PRs

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing CSP origin derivation for release-backed content.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/derived-csp-release-context

Comment @coderabbitai help to get the list of available commands.

@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: 30cefedf07

ℹ️ 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 src/platform/adapters/fs/veryfront/adapter.ts Outdated
Comment thread src/server/runtime-handler/derive-project-csp.test.ts Outdated
Two review findings, both real.

The warmup fetched the files and then the reread depended on them having been
stored. Caching can be disabled outright, and a backend write can fail, in
which case the fetch succeeded and the caller still saw nothing -- the same
empty derivation on every request that this PR exists to fix. The warmup now
resolves with what it fetched and the caller takes that, falling back to the
cache only when it has nothing.

The snapshot test asserted nothing: it looped over an array that was never
populated, and it used two release ids, whose differing prefixes made the key
distinct even with the promise stringified. So it would have passed against the
bug. Replaced with a fixed release identity and a moving snapshot, which is the
only shape that can see it, and confirmed it fails when the await is removed.

That is the third vacuous assertion found in this area, after the dead literals
in the skill runtime tests and the camel-case payload that exercised its own
bug without seeing it. This one was mine.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/adapters/fs/veryfront/adapter.ts`:
- Around line 711-720: Update getAllSourceFiles so failures from
this.cache.setAsync are caught and logged separately while preserving and
returning the successfully fetched files. Keep the existing warmup/read-failure
handling that returns null, and add a focused test covering a rejecting cache
write and verifying the fetched files are still returned.
- Around line 1076-1091: Bind the warmup awaited in the file-list read path to
the captured cacheKey instead of using the mutable singleton
this.fileListWarmupPromise. Update warmup scheduling and consumption around the
relevant file-list methods to store per-key promises or use a key-tagged handle,
and only apply fetched files when the key matches; otherwise read the requested
key’s cache. Add a test covering an interleaved context switch and verifying CSP
origins use the original source snapshot.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4813ed3d-f68f-4fa7-a7e7-de2272ac657e

📥 Commits

Reviewing files that changed from the base of the PR and between 7773c91 and 20fa4b3.

📒 Files selected for processing (4)
  • src/platform/adapters/fs/veryfront/adapter.ts
  • src/platform/adapters/fs/veryfront/multi-project-adapter.ts
  • src/server/runtime-handler/derive-project-csp.test.ts
  • src/server/runtime-handler/project-runtime-context.ts

Comment on lines +711 to +720

return files;
} catch (error) {
logger.warn("File list warmup failed", {
reason,
cacheKey: effectiveCacheKey,
error: error instanceof Error ? error.message : String(error),
});

return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve fetched files when the cache write fails.

If this.cache.setAsync rejects on Line 696, this catch returns null after a successful fetch. getAllSourceFiles then cannot use the fetched files and can only retry the failed cache read. CSP derivation remains empty in the cache-write-failure case that this change must support.

Catch and log cache-write failures separately. Continue with files. Add a focused test with a rejecting cache write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/adapter.ts` around lines 711 - 720, Update
getAllSourceFiles so failures from this.cache.setAsync are caught and logged
separately while preserving and returning the successfully fetched files. Keep
the existing warmup/read-failure handling that returns null, and add a focused
test covering a rejecting cache write and verifying the fetched files are still
returned.

Comment on lines +1076 to +1091
let files = cached?.files;

// A miss schedules a warmup and returns immediately, which is right for
// callers that can proceed without the list. This one cannot: nothing else
// populates it for a release-backed context, so returning early meant the
// list was empty on every request for the life of the process. Wait for the
// fetch this read just started, then look again.
if (options.waitForWarmup && cacheKey && !files?.length && this.fileListWarmupPromise) {
// Take what the fetch returned rather than re-reading the cache: with
// caching disabled, or a failed backend write, the cache keeps nothing
// and correctness would depend on a write that never happened.
const fetched = await this.fileListWarmupPromise;
files = fetched?.length
? fetched
: await this.cache.getAsync<{ path: string; content?: string }[]>(cacheKey);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bind the waited warmup to cacheKey.

cacheKey is captured before this block, but this.fileListWarmupPromise is a mutable singleton. If another content context starts a warmup before Line 1087, this call can await that other warmup and return its files for the original cache key. This can derive CSP origins from the wrong source snapshot.

Store warmups by cache key, or return a key-tagged warmup handle from scheduling and consume it only when its key matches cacheKey. Add an interleaved context-switch test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/adapter.ts` around lines 1076 - 1091, Bind
the warmup awaited in the file-list read path to the captured cacheKey instead
of using the mutable singleton this.fileListWarmupPromise. Update warmup
scheduling and consumption around the relevant file-list methods to store
per-key promises or use a key-tagged handle, and only apply fetched files when
the key matches; otherwise read the requested key’s cache. Add a test covering
an interleaved context switch and verifying CSP origins use the original source
snapshot.

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 82cc594 Aug 8, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/derived-csp-release-context branch August 8, 2026 22:58
@kojiwakayama kojiwakayama mentioned this pull request Aug 8, 2026
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