Restore openai/widgetCSP, and make the check assert the key ChatGPT reads - #100
Conversation
…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.
QA Review: fix/chatgpt-widget-cspScope: 13 files, +270/−44 · worktree: The core is right and I reproduced it end to end. Against a live server built on Critical (must fix)
Fix in-PR (apply mechanically — no adjudication essay expected)
Suggestions (optional)
What Looks Good
Body claims
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.
Adjudication round 1: Critical's consequence accepted, its remedy declined as new API · fixed at
|
QA Review: fix/chatgpt-widget-cspScope: 11 files, +74/−35 (delta) · worktree: The delta is prose, a version cut, and one test assertion — no runtime code changed. I re-ran everything anyway: Fix verification
Fix in-PR (apply mechanically — no adjudication essay expected)
What Looks Good
Body claims
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"
} |
r2f1 applied — #101 carries the deferral now ·
|
What
The
ui://resource emitsopenai/widgetCSPandopenai/widgetPrefersBorderagain,derived from the same mapping that builds
ui.csp/ui.prefersBorder.synapse checkgains
resource-openai-csp(error forchatgpt) andresource-openai-data-fonts(warn),which read that dialect;
resource-cspdrops to warn forchatgptandresource-data-fontsleaves that profile.Why
0.7.0 dropped the aliases on the claim that
ui.cspcarried them. Measured 2026-09-17 indeveloper mode: a resource carrying
ui.cspalone got no policy at all in ChatGPT(
CSP offbadge). An empty allowlist — "reach nothing" — had become unrestricted egress.The test asserting the key went in the same change, and
resource-cspasserted the speckey for
chatgptwith a reason saying ChatGPT read it, so the gate passed the brokenserver. Fixing the emitter alone leaves that blind spot.
Release
npm 0.21.0 (a new
errorrule fails runs that passed — a behaviour change, not anaddition) and Python 0.7.1 (emitter-only, additive).
Test
npm run ci566/37 green ·npm run conformance36/36 · Python 37 passed, ruff clean,tyidentical 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.