Skip to content

Restore openai/widgetCSP, and make the check assert the key ChatGPT reads - #100

Merged
mgoldsborough merged 3 commits into
mainfrom
fix/chatgpt-widget-csp
Sep 17, 2026
Merged

mgoldsborough merged 3 commits into
mainfrom
fix/chatgpt-widget-csp

Conversation

@mgoldsborough

@mgoldsborough mgoldsborough commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

What

The ui:// resource emits openai/widgetCSP and openai/widgetPrefersBorder again,
derived from the same mapping that builds ui.csp / ui.prefersBorder. synapse check
gains resource-openai-csp (error for chatgpt) and resource-openai-data-fonts (warn),
which read that dialect; resource-csp drops to warn for chatgpt and
resource-data-fonts leaves that profile.

Why

0.7.0 dropped the aliases on the claim that ui.csp carried them. Measured 2026-09-17 in
developer mode: a resource carrying ui.csp alone got no policy at all in ChatGPT
(CSP off badge). An empty allowlist — "reach nothing" — had become unrestricted egress.
The test asserting the key went in the same change, and resource-csp asserted the spec
key for chatgpt with a reason saying ChatGPT read it, so the gate passed the broken
server. Fixing the emitter alone leaves that blind spot.

Release

npm 0.21.0 (a new error rule fails runs that passed — a behaviour change, not an
addition) and Python 0.7.1 (emitter-only, additive).

Test

npm run ci 566/37 green · npm run conformance 36/36 · Python 37 passed, ruff clean, ty
identical to main · vendored IIFE rebuilt byte-identical. CI is the record.

Known gap, documented not guessed at: both dialects carry two of the four origin lists each
defines, as they have since the key was first emitted — #101.

…eads

0.7.0 dropped the resource's `openai/widgetCSP` and `openai/widgetPrefersBorder`
aliases on the claim that `ui.csp` and `ui.prefersBorder` carry both. The claim
was never measured and is false: ChatGPT reads `openai/widgetCSP` and ignores the
spec's nested `ui.csp`, so with the alias gone it has no policy to apply and runs
the widget under none at all. Observed live on 2026-09-17 — a resource serving
`ui.csp` alone rendered with a `CSP off` badge beside the connector; 0.3.x, which
emitted the alias, showed no badge.

It is a security-posture regression, not a render one: an empty allowlist ("reach
nothing") became unrestricted egress in ChatGPT. Nothing went red because the test
asserting the key was deleted in the same change, and because `resource-csp`
asserted `ui.csp` for the `chatgpt` target and printed the same false sentence as
its reason — so the gate passed a frame the host was leaving unprotected.

Both dialects are now derived from one set of inputs in `_chatgpt_resource_aliases`,
the precedent `_chatgpt_tool_aliases` set: an alias derived from the spec value
cannot contradict it, and it is emitted always rather than conditionally because
ChatGPT's default here — no policy — differs from the spec's.

`synapse check` gets `resource-openai-csp` (error for `chatgpt`) rather than making
`resource-csp` target-aware, so each host's requirement keeps its own rule and its
own reason; a check meaning different things per profile is how the wrong reason
stayed invisible. It also fails a camelCase alias, since origins under a key ChatGPT
does not read are origins it does not allow. `resource-openai-data-fonts` checks a
`data:` font against the allowlist that target consults. `resource-csp` stays for
`chatgpt` as a portability warning, not an error: ChatGPT does not consult it, and
whether app submission requires it is unmeasured — asserting that again is the
mistake being fixed.

Releases npm 0.20.1 and Python 0.7.1: additive, no API change.
@mgoldsborough

Copy link
Copy Markdown
Contributor Author

QA Review: fix/chatgpt-widget-csp

Scope: 13 files, +270/−44 · worktree: .claude/worktrees/fix-chatgpt-widget-csp · reviewed: 1b2ea57
Round: 1 (full trial)
Risk: MEDIUM
Why this exists: 0.7.0 dropped openai/widgetCSP on an unmeasured claim that ui.csp carried it, leaving every ChatGPT frame with no policy at all, and deleted the test that would have caught it — this restores the key and closes the blind spot in synapse check that passed the broken server.

The core is right and I reproduced it end to end. Against a live server built on main's emitter, synapse check --target chatgpt reports FAIL resource-openai-csp — ui://e2e/view declares no openai/widgetCSP; against one built on this branch, PASS. Mutation checks confirm the new tests can fail: camelCasing the alias key reddens 5 Python tests, dropping the alias reddens 6, removing the check's camelCase branch reddens 1, and downgrading resource-openai-csp to warn reddens 1. The cross-language seam that let 0.7.0 ship — Python emits, TS asserts — is now covered on both sides and agrees.

Critical (must fix)

  • r1c1 [python/nimblebrain_synapse/server.py:428] The emitted openai/widgetCSP declares only two of its four origin lists, and turning the policy on makes the other two enforcing with no way to declare them [reasoned] — OpenAI's Apps SDK reference documents openai/widgetCSP as carrying connect_domains, resource_domains, frame_domains and redirect_domains, and says that to allowlist redirect targets for window.openai.openExternal() "you must still set _meta["openai/widgetCSP"].redirect_domains" — _meta.ui.csp has no equivalent. _chatgpt_resource_aliases emits the first two only, and SynapseUI has no attribute for the other two, so the policy is incomplete by construction. Under 0.7.0 that cost nothing, because ChatGPT applied no policy; this PR's whole premise is that the key is now enforced, and the same premise makes the absent lists enforced too. The component can reach window.openai directly — src/host/detect.ts:21 notes ChatGPT injects it into every frame it serves — so this is reachable without any SDK surface for it.
    · Preconditions: consumer upgrades nimblebrain-synapse 0.7.0 → 0.7.1 → their ChatGPT component frames a third-party origin or calls window.openai.openExternal() → openai/widgetCSP is now present and enforcing → frame_domains / redirect_domains are undeclared → the frame or the external open is blocked, silently, with no SynapseUI kwarg that can allow it.
    · Fix: add optional frame_domains / redirect_domains that ride into openai/widgetCSP when supplied (frame_domains also into ui.csp.frameDomains, which the spec does have). If ChatGPT's behaviour for an absent list is unmeasured, then say exactly that in the docstring and the changelog rather than shipping the incompleteness undocumented — that is this PR's own standard, applied to its own new key.

Fix in-PR (apply mechanically — no adjudication essay expected)

  • r1f1 [src/check/profiles.ts:100] "ChatGPT ignores ui.csp" is stated as a property of the host, but the vendor documents the opposite direction — soften to what was measured. OpenAI's Apps SDK reference lists _meta.ui.csp as the standard surface and says it is "generally preferred for new UI", with openai/widgetCSP as a legacy compatibility key ChatGPT reads when present. Your measurement supports "on 2026-09-17, in developer mode, ui.csp alone yielded no policy"; it does not support the absolute, and the absolute is what the new why rests on ("ui.csp is ... for every host that reads the spec key", i.e. portability only). That leaves resource-csp printing a wrong reason for chatgpt — the same defect class the PR exists to remove — and drops the vendor's preferred key to optional for that target. Same sentence in CLAUDE.md:180, both changelogs, and the PR body. Emitting both dialects is correct under either reading, so only the claim needs narrowing.
  • r1f2 [PR body, "Release"] "additive, no API change, no consumer code change" is wrong for the npm half — a new error-severity rule changes synapse check --target chatgpt from pass to fail for every server on nimblebrain-synapse < 0.7.1 (reproduced above), resource-csp goes error→warn, and resource-data-fonts leaves the profile. The CHANGELOG entry implies this; the body contradicts it. One line naming it, and a call on whether a new error rule belongs in 0.20.1 rather than 0.21.0.

Suggestions (optional)

  • r1s1 [src/check/index.ts:398] With no openai/widgetCSP at all and a data: font in the HTML, both resource-openai-csp (error) and resource-openai-data-fonts (warn) fire, and the warn's detail says openai/widgetCSP.resource_domains omits data: about a key that does not exist. By the PR body's own reasoning the font is inert in that state, so the warn reports a non-problem next to the error that is the problem. Consider skipping a dialect's font check when that dialect's policy key is missing.
  • r1s2 [src/check/index.ts:530] isOriginLists accepts {}, so openai/widgetCSP: {} passes resource-openai-csp. Harmless if ChatGPT reads an empty object as empty allowlists; unmeasured otherwise. Same latitude resource-csp already had, so not a regression.
  • r1s3 [src/__tests__/check/check.test.ts:113] The warn-vs-unlisted distinction for resource-csp on chatgpt is not pinned: the assertion is chatgpt.failed === false, which holds whether the rule is warn or absent from the profile. A later sweep could drop it and stay green.

What Looks Good

  • The alias is derived from the same mapping _ui_resource_meta() builds, so the two dialects cannot name different origins — and the tests pin the spelling, not just the values, which is the failure mode that actually bites.
  • A separate resource-openai-csp rather than a target-aware resource-csp. A check that means different things per profile is exactly how the wrong reason stayed invisible; one host requirement, one rule, one sentence.
  • Reporting a camelCase openai/widgetCSP as a failure. Origins under a key the host does not read look declared and allow nothing — the quietest version of this bug, and it is now caught.
  • Declining to add openai/* rules for nimblebrain and claude on inference. Adding an unmeasured rule is the thing being fixed.

Body claims

  • ✅ npm run ci green, 566 tests / 37 files — ran it; 566 passed, 37 files. Lint exit 0, typecheck, build, size budget all green.
  • ✅ npm run conformance 36/36 — ran it; 36/36.
  • ✅ Python 37 passed, ruff check/format clean — ran all three.
  • ✅ ty shows the same 4 pre-existing diagnostics as main — diffed the sorted diagnostic lists against an origin/main worktree; identical.
  • ✅ __client_version__ matches package.json — both 0.20.1; the ci.yml gate agrees.
  • ✅ Vendored client IIFE byte-identical — sha256 of dist/synapse-ui.iife.global.js, the vendored copy, and origin/main's vendored copy all 62a9ef9c….
  • ✅ synapse check --target chatgpt now fails a server that omits the key — reproduced against live servers on both emitters.
  • ✅ package-lock.json root version stale at 0.19.0, out of charter — confirmed stale on main too.
  • ⚠️ "ChatGPT reads openai/widgetCSP; it ignores the nested ui.csp" — the measurement supports the first half; the vendor documents ui.csp as the preferred standard and the alias as legacy. See r1f1.
  • ❌ "additive, no API change, no consumer code change" — true of the Python emitter, not of the check CLI. See r1f2.
machine record
{
  "kind": "review", "pr": 100, "round": 1,
  "reviewed": "1b2ea57e0c975cddc1dfb080ef4b6d75bcabbbf0", "delta_base": null,
  "risk": "MEDIUM", "stop_gate": "n/a",
  "verdict": "Core reproduced and correct; one Critical in the core — the emitted policy omits frame_domains/redirect_domains and the SDK cannot declare them, so turning the policy on silently removes a capability with no escape hatch.",
  "findings": [
    {"id": "r1c1", "bucket": "critical", "file": "python/nimblebrain_synapse/server.py", "line": 428,
     "claim": "openai/widgetCSP is emitted with only connect_domains/resource_domains; frame_domains and redirect_domains are absent and SynapseUI has no attribute for them, so enabling the policy makes their absence enforcing with no supported way to declare them.",
     "evidence": "reasoned",
     "receipt": "OpenAI Apps SDK reference documents openai/widgetCSP fields connect_domains, resource_domains, frame_domains, redirect_domains, and states redirect_domains must be set on this key for window.openai.openExternal() (ui.csp has no equivalent); grep for openExternal/redirect_domains/frame_domains across src, python, web/src, gallery, docs returned zero hits.",
     "preconditions": "upgrade 0.7.0->0.7.1 -> component frames a third-party origin or calls window.openai.openExternal() -> openai/widgetCSP now present and enforcing -> frame_domains/redirect_domains undeclared -> blocked, no kwarg to allow it",
     "fix": "add optional frame_domains/redirect_domains kwargs into the alias (and ui.csp.frameDomains), or document the incompleteness explicitly if the absent-list semantics are unmeasured",
     "scope": "core"},
    {"id": "r1f1", "bucket": "fix-in-pr", "file": "src/check/profiles.ts", "line": 100,
     "claim": "The 'ChatGPT ignores ui.csp' claim is stated as a host property, but OpenAI documents _meta.ui.csp as the preferred standard and openai/widgetCSP as a legacy compatibility key; the measurement only supports a dated, developer-mode observation.",
     "evidence": "reproduced",
     "receipt": "Fetched developers.openai.com/apps-sdk/reference: ui.csp is 'generally preferred for new UI'; openai/widgetCSP described as 'Legacy ChatGPT compatibility key'.",
     "preconditions": null,
     "fix": "date and scope the claim in profiles.ts why, CLAUDE.md, both changelogs and the body; name the alias's legacy status",
     "scope": "peripheral"},
    {"id": "r1f2", "bucket": "fix-in-pr", "file": "CHANGELOG.md", "line": 9,
     "claim": "The body's 'additive, no API change, no consumer code change' is wrong for the npm half: a new error-severity rule turns passing chatgpt runs into failures, resource-csp drops error->warn, and resource-data-fonts leaves the chatgpt profile.",
     "evidence": "reproduced",
     "receipt": "synapse check --target chatgpt against a live server on origin/main's emitter: FAIL resource-openai-csp; against this branch's emitter: PASS.",
     "preconditions": null,
     "fix": "one CHANGELOG line naming the behaviour change; decide 0.20.1 vs 0.21.0",
     "scope": "peripheral"},
    {"id": "r1s1", "bucket": "suggestion", "file": "src/check/index.ts", "line": 398,
     "claim": "When openai/widgetCSP is absent, resource-openai-data-fonts also fails and its detail names a key that does not exist, for a font the PR body says is inert in that state.",
     "evidence": "reproduced",
     "receipt": "Read the hasDataFont branch: declaresData(undefined, 'resource_domains') is false, so both checks push.",
     "preconditions": null, "fix": "skip a dialect's font check when that dialect's policy key is missing", "scope": "peripheral"},
    {"id": "r1s2", "bucket": "suggestion", "file": "src/check/index.ts", "line": 530,
     "claim": "isOriginLists accepts {}, so an empty openai/widgetCSP passes resource-openai-csp.",
     "evidence": "reproduced",
     "receipt": "Object.values({}).every(...) is true, so the guard admits {}.",
     "preconditions": null, "fix": "require at least the two origin lists, if ChatGPT's reading of {} is known", "scope": "peripheral"},
    {"id": "r1s3", "bucket": "suggestion", "file": "src/__tests__/check/check.test.ts", "line": 113,
     "claim": "No test distinguishes resource-csp being warn for chatgpt from being absent from the profile.",
     "evidence": "reproduced",
     "receipt": "The assertion is chatgpt.failed === false, which holds for warn and for unlisted alike.",
     "preconditions": null, "fix": "assert the rule is present at warn in the chatgpt report", "scope": "peripheral"}
  ]
}

The claim "ChatGPT ignores ui.csp" is a property of the host, and the measurement
does not reach that far. What was measured, on 2026-09-17 in developer mode: a
resource carrying `ui.csp` alone got no policy at all. OpenAI's reference documents
`ui.csp` as generally preferred for new UI and `openai/widgetCSP` as a legacy
compatibility key it reads when present — the opposite emphasis. Emitting both is
right under either reading; only the claim was overreaching, and it was load-bearing
for `resource-csp` dropping to `warn` on a "portability only" reason. Narrowed
wherever it was stated. `warn` still holds, now for the reason that fits it: a
requirement the vendor documents and the host was not observed to enforce is exactly
what `warn` means in this profile system, and a test now pins the severity rather
than only the run's exit.

A new `error` rule is not additive. `synapse check --target chatgpt` now fails every
server emitting `ui.csp` alone, `resource-csp` goes error→warn, and
`resource-data-fonts` leaves the profile — so the npm half is 0.21.0, not 0.20.1.
Consumers pin caret on 0.x, which does not cross a minor; shipping a behaviour change
as a patch carries it silently to every one of them. Python stays 0.7.1, where the
emitter change really is additive.

Both dialects carry two of the four origin lists each defines, as they have since the
key was first emitted — and where 0.7.0 left the ChatGPT frame unpoliced, that
omission now has whatever weight the host gives an absent list. Nobody has measured
what that is, and neither vendor documents it, so it is written down rather than
guessed at: docstring, changelog, and #101 for the surface once it is measured.
@mgoldsborough

Copy link
Copy Markdown
Contributor Author

Adjudication round 1: Critical's consequence accepted, its remedy declined as new API · fixed at 5ee2be6

r1c1 · valid consequence, remedy out of charter

premise: accepted — the absent-list semantics are undocumented on both sides. Your own
receipt says OpenAI documents no absent-case behaviour; the spec documents its own as a
secure default. So "the absent lists become enforcing" is unverified.

Two things narrow it further. The gap is four lists per dialect, not two — ui.csp
also defines frameDomains and baseUriDomains — so it is symmetric across both
dialects, not a ChatGPT incompleteness this PR introduces. And SynapseUI has never
exposed any of them, including across the whole 0.3.x–0.6.x line that emitted
openai/widgetCSP with exactly these two fields. What is newly true is only relative to
0.7.0, which had no policy at all — and that is worth writing down.

Taking your stated alternative rather than the kwargs: added kwargs are new public API
invented under review pressure, and redirect_domains is ChatGPT-only while
baseUriDomains is spec-only, so the signature has to name which dialect each reaches —
one considered pass, not two bolted on. Documented in the _chatgpt_resource_aliases
docstring and the Python changelog, with the surface tracked in #101 (which says to
measure first).

Fix in-PR

  • r1f1 applied, 5ee2be6. You are right and it is the defect class this PR exists to
    remove. Narrowed to the measurement — "2026-09-17, developer mode: ui.csp alone got no
    policy" — in profiles.ts, check/index.ts, server.py, the Python test docstring,
    CLAUDE.md, both changelogs, cli.mdx and the body, and the vendor's own framing
    (ui.csp preferred, the alias legacy) now appears beside it. The severity call is
    unchanged and better justified: a requirement the vendor documents and the host was not
    observed to enforce is precisely what warn means here.
  • r1f2 applied, 5ee2be6. Called it 0.21.0, not 0.20.1. Consumers pin caret on
    0.x, which does not cross a minor, so a behaviour change cut as a patch reaches every
    one of them silently — this repo's own release note says so. CHANGELOG entry moved to
    ### Changed and names the break and the upgrade path. Python stays 0.7.1.

Suggestions

  • r1s3 taken, 5ee2be6 — one assertion on the resource-csp severity in the
    chatgpt report. It pins the exact decision r1f1 is about, which is worth a line.
  • r1s1, r1s2 declined. Both are real; neither is worth a branch about restoring a
    dropped key, and r1s2's fix needs a measurement nobody has.

Noted on method: thank you for catching the .venv editable-install artefact yourself
rather than filing it. A "the guard doesn't guard" finding would have cost a full
re-derivation here.

Verification

CI run 35287523187:
all 8 jobs pass. Not re-measured beyond CI — the code change this round is one test
assertion; everything else is prose and version strings. The vendored IIFE is unchanged
and still byte-identical to dist/.

machine record
{
  "kind": "adjudication", "pr": 100, "round": 1, "fixed_at": "5ee2be6",
  "verdicts": [
    {"id": "r1c1", "verdict": "valid", "premise": "accepted",
     "resolution": "Consequence documented, remedy declined as new public API. Absent-list semantics undocumented by both vendors (per the finding's own receipt), so the premise is unverified; the gap is four lists per dialect (ui.csp also defines frameDomains/baseUriDomains), symmetric across both, and predates this PR — 0.3.x-0.6.x emitted openai/widgetCSP with the same two fields. Documented in the _chatgpt_resource_aliases docstring and the Python changelog; kwarg surface tracked in #101, which requires measurement first."},
    {"id": "r1f1", "verdict": "valid", "premise": "verified",
     "resolution": "applied 5ee2be6 — claim narrowed to the 2026-09-17 developer-mode measurement in profiles.ts, check/index.ts, server.py, the Python test docstring, CLAUDE.md, both changelogs, cli.mdx and the PR body; vendor framing (ui.csp preferred, alias legacy) stated beside it. resource-csp stays warn, now justified by the profile system's own definition of warn."},
    {"id": "r1f2", "verdict": "valid", "premise": "verified",
     "resolution": "applied 5ee2be6 — npm cut as 0.21.0 rather than 0.20.1; CHANGELOG entry moved to ### Changed and names the behaviour change plus the upgrade path. Python stays 0.7.1."},
    {"id": "r1s1", "verdict": "out-of-charter", "premise": "accepted", "resolution": "declined"},
    {"id": "r1s2", "verdict": "out-of-charter", "premise": "accepted", "resolution": "declined — the fix needs a measurement of how ChatGPT reads an empty object"},
    {"id": "r1s3", "verdict": "valid", "premise": "verified",
     "resolution": "applied 5ee2be6 — check.test.ts asserts resource-csp is present at warn in the chatgpt report, not merely that the run did not fail"}
  ],
  "ci": "pass — all 8 jobs, run 35287523187"
}

@mgoldsborough

Copy link
Copy Markdown
Contributor Author

QA Review: fix/chatgpt-widget-csp

Scope: 11 files, +74/−35 (delta) · worktree: .claude/worktrees/fix-chatgpt-widget-csp · reviewed: 5ee2be6
Round: 2 (delta base: 1b2ea57)
Stop gate: SHIP THE CORE — the Critical bucket is empty this round; the one finding is peripheral and sits outside the diff.
Risk: LOW

The delta is prose, a version cut, and one test assertion — no runtime code changed. I re-ran everything anyway: npm test 566/37, Python pytest 37, ruff check + format --check clean, ty check nimblebrain_synapse clean, npm run build then diff dist/synapse-ui.iife.global.js python/nimblebrain_synapse/_assets/synapse-ui.iife.js identical at 62a9ef9c… — the same hash as round 1, so the version bump is not baked into the IIFE and the vendored copy cannot go stale on a release. Conformance not re-run locally (no runtime code in the delta); CI run 35287523187 is green on all 8 jobs on this exact SHA.

Fix verification

  • r1f1 — landed, and the sweep is complete. grep -rni "ignores \?ui.csp|does not consult|does not read the"acrosssrc/, python/, web/, CLAUDE.md and both changelogs returns nothing; the only "and ignores" left is an unrelated 0.x mcp-ui entry. The measurement is now dated and scoped in all eight places, and the vendor's framing (ui.csppreferred, the alias legacy) sits beside it. Thewarnjustification inprofiles.ts:101— "a requirement the vendor documents and the host was not observed to enforce is whatwarnis for" — is the right shape: it argues from the profile system's own definition rather than from a host property nobody measured.SPEC_CSPis referenced only by thechatgptprofile, so the ChatGPT-specific measurement does not leak intonimblebrain/claude` reasons.
  • r1f2 — landed, and the call is right. package.json and __client_version__ both 0.21.0, CHANGELOG heading and ### Changed section agree, main is 0.20.0 so this is a clean minor. The entry names the break and the upgrade path in the two sentences a consumer needs. Caret-on-0.x is the correct reason and it is the decisive one.
  • r1s3 — landed and it bites. Mutation-checked in a throwaway tree: deleting resource-csp from the chatgpt profile reddens 1 test; flipping its severity warn→error reddens 1 test. applyProfiles (src/check/index.ts:126) overwrites severity from the rule, so the assertion reads the profile-applied value, and the optional chain makes an absent rule undefined !== "warn" rather than a silent pass.
  • r1c1 — premise re-checked, and the branch's version of it is now the verifiable one. The declined-remedy argument turns on two claims and both hold: the ext-apps schema vendored in node_modules/@modelcontextprotocol/ext-apps/dist/src/generated/schema.json:410-431 defines resourceDomains, frameDomains and baseUriDomains alongside connectDomains, and documents the omitted case as frame-src 'none' / base-uri 'self' — so the gap is four lists per dialect, symmetric, and the spec side's absent-list reading is a secure default, exactly as the docstring says. SynapseUI.__init__ takes connect_domains and resource_domains and nothing else, and git log -S widgetCSP -- python/ puts the first emission at 784eef9 with the same two fields, so "as they have since the key was first emitted" is accurate. Declining to invent kwargs under review pressure was the right call and the docstring now carries the thing a reader needs.

Fix in-PR (apply mechanically — no adjudication essay expected)

  • r2f1 [SynapseUI cannot declare frame/redirect/base-uri origins in either CSP dialect #101] The issue this PR defers the gap to has an empty body (-) and no labels [reproduced] — gh issue view 101 --json title,body,labels returns {"body":"-","labels":[]}. The title is right, but everything that makes the deferral real is missing: the four list names, which dialect each belongs to, why redirect_domains is not derivable from ui.csp, and the measurement that has to happen before a signature can be designed. The adjudication's machine record says "SynapseUI cannot declare frame/redirect/base-uri origins in either CSP dialect #101, which requires measurement first" — that sentence is not in SynapseUI cannot declare frame/redirect/base-uri origins in either CSP dialect #101. Both python/CHANGELOG.md:36 and the PR body send a reader there, and a reader who follows either lands on a bare title. This is the same shape the PR exists to close: a claim whose backing is not where it says it is. Fix: paste the docstring's paragraph into the body, state the consequence in one line (a component that frames a third-party origin or calls window.openai.openExternal() has no way to declare it), name the measurement that gates a design, and label it. Repo is public, so keep it to in-repo paths and the two vendors' public docs — everything needed already is.

What Looks Good

  • The narrowing is a genuine downgrade of confidence, not a hedge. "Measured 2026-09-17, developer mode" is falsifiable, dated, and re-runnable; "ChatGPT ignores ui.csp" was none of those. The why strings now tell a developer which evidence they are relying on, which is the difference between a check they can trust and one they route around.
  • Minor rather than patch, argued from consumer pinning rather than from taste. A new error rule reaching every caret-pinned consumer silently is precisely the failure this repo's own release note warns about.
  • The #101 paragraph is written as a known gap with its blast radius stated — self-contained components unaffected, framing/external-opening components not declarable. That is the honest version, and it is in the changelog where a consumer reads it rather than only in a tracker.
  • The severity assertion pins the decision r1f1 is about, not just the run's outcome. A later sweep that drops resource-csp from the chatgpt profile now reddens instead of staying green.

Body claims

  • ✅ npm 0.21.0 / Python 0.7.1 — package.json, __client_version__, both changelogs agree; main is 0.20.0.
  • ✅ "npm run ci 566/37 green · Python 37 passed, ruff clean" — re-ran all of it.
  • ✅ "ty identical to main" — ty check nimblebrain_synapse passes clean (the 4 diagnostics seen in round 1 were the .venv editable-install artefact, since removed from the run).
  • ✅ "vendored IIFE rebuilt byte-identical" — rebuilt; diff clean, 62a9ef9c… unchanged from round 1.
  • ✅ "conformance 36/36" — not re-run locally; CI run 35287523187 green on this SHA.
  • ⚠️ "Known gap … — SynapseUI cannot declare frame/redirect/base-uri origins in either CSP dialect #101" — the gap is documented accurately in the changelog and docstring; SynapseUI cannot declare frame/redirect/base-uri origins in either CSP dialect #101 itself is empty. See r2f1.
machine record
{
  "kind": "review", "pr": 100, "round": 2,
  "reviewed": "5ee2be6708598d52f38d6411f88fe0e8434345ec", "delta_base": "1b2ea57e0c975cddc1dfb080ef4b6d75bcabbbf0",
  "risk": "LOW", "stop_gate": "ship-the-core",
  "verdict": "All three accepted fixes landed and hold; r1c1's declined-remedy argument verified against the vendored ext-apps schema and git history. No Criticals. One peripheral Fix-in-PR: the issue the deferral points at is empty.",
  "findings": [
    {"id": "r2f1", "bucket": "fix-in-pr", "file": "python/CHANGELOG.md", "line": 36,
     "claim": "Issue #101, cited by both the Python changelog and the PR body as where the four-origin-list gap is tracked, has an empty body ('-') and no labels; the adjudication's claim that it 'requires measurement first' is not recorded there.",
     "evidence": "reproduced",
     "receipt": "gh issue view 101 --json title,body,labels -> {\"body\":\"-\",\"labels\":[],\"state\":\"OPEN\"}; title is correct.",
     "preconditions": null,
     "fix": "paste the _chatgpt_resource_aliases paragraph into #101's body, state the consequence in one line, name the measurement that gates a signature, and label it",
     "scope": "peripheral"}
  ],
  "verified_fixes": [
    {"id": "r1f1", "status": "landed", "receipt": "grep for absolute 'ignores ui.csp' / 'does not consult' across src, python, web, CLAUDE.md, both changelogs: no hits; SPEC_CSP referenced only by the chatgpt profile."},
    {"id": "r1f2", "status": "landed", "receipt": "package.json 0.21.0, __client_version__ 0.21.0, CHANGELOG ### Changed names the break and upgrade path; origin/main is 0.20.0."},
    {"id": "r1s3", "status": "landed", "receipt": "mutation: delete resource-csp from chatgpt profile -> 1 test red; flip warn->error -> 1 test red."},
    {"id": "r1c1", "status": "premise-verified", "receipt": "ext-apps generated/schema.json:410-431 defines connectDomains/resourceDomains/frameDomains/baseUriDomains with omitted-case frame-src 'none' / base-uri 'self'; SynapseUI.__init__ takes only the two; git log -S widgetCSP -- python/ dates first emission to 784eef9 with the same two fields."}
  ],
  "ci": "pass — all 8 jobs, run 35287523187",
  "local": "npm test 566/37 pass; pytest 37 pass; ruff check + format clean; ty clean; build + vendored IIFE diff clean at 62a9ef9c"
}

@mgoldsborough

Copy link
Copy Markdown
Contributor Author

r2f1 applied — #101 carries the deferral now · qa-reviewed

Applied by the reviewer, so flagging the dual role: this is the one fix I raised and also
implemented, and nothing in the branch changed. 5ee2be6 is still the reviewed and merge
candidate SHA.

#101 now holds what the citation
promised: the four-lists-per-dialect table with each list's CSP directive, the two
asymmetries that make this one signature rather than four kwargs (baseUriDomains is
spec-only, redirect_domains is ChatGPT-only and the only route to an
openExternal() target), the consequence in its own section — a component that nests a
third-party iframe or opens an external link cannot declare the origin, and cannot reach
around SynapseUI to do it by hand — and the measurement that gates a design, with the
reason it gates rather than decorates. Receipts are all checkable from a clean install:
the vendored ext-apps schema, server.py, and git log -S widgetCSP. Labelled
enhancement. This repo has no admission label, so treat it as filed, not scheduled.

Adding qa-reviewed. Round 2 closed with an empty Critical bucket, all three accepted
round-1 fixes verified landed, r1c1's declined-remedy argument checked against the vendored
schema and git history rather than taken on its word, and CI green on all 8 jobs at
5ee2be6. Two round-1 suggestions stay declined and unfiled by design — r1s1 (the font
warn firing beside the error that is the real problem) and r1s2 (isOriginLists admits
{}) are both real and neither has a consequence anyone can name, so they live in the
round-1 comment rather than as rows nobody will pick up.

machine record
{
  "kind": "adjudication", "pr": 100, "round": 2, "fixed_at": "5ee2be6",
  "note": "No branch change. The round-2 finding was filed against an issue, not the diff; applied by the reviewer.",
  "verdicts": [
    {"id": "r2f1", "verdict": "valid", "premise": "verified",
     "resolution": "applied — issue #101 body written (four-list table per dialect with CSP directives, the spec-only/ChatGPT-only asymmetries, consequence section, gating measurement, checkable receipts) and labelled enhancement. No admission label exists in this repo; filed, not scheduled."}
  ],
  "label": "qa-reviewed",
  "ci": "pass — all 8 jobs, run 35287523187",
  "unfiled_by_design": ["r1s1", "r1s2"]
}

@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Sep 17, 2026
@mgoldsborough
mgoldsborough merged commit d9ab34b into main Sep 17, 2026
8 checks passed
@mgoldsborough
mgoldsborough deleted the fix/chatgpt-widget-csp branch September 17, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant