diff --git a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-contract.ts b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-contract.ts index cbef539464c..781933ee7ac 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-contract.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-contract.ts @@ -46,6 +46,7 @@ export interface ChatgptReceipt { requestedScopes?: string[]; acceptedScopes?: string[]; configureDisclosed?: boolean; + consentWording?: string; } const uuid = (s: unknown) => typeof s === "string" && @@ -135,6 +136,7 @@ export function validChatgptReceipt(r: ChatgptReceipt, i: ChatgptInput) { "requestedScopes", "acceptedScopes", "configureDisclosed", + "consentWording", ]; return ( !!r && @@ -162,6 +164,11 @@ export function validChatgptReceipt(r: ChatgptReceipt, i: ChatgptInput) { r.actualProviderCallback === true && r.boundMcpComment === true && r.zeroNativeAdmissions === true && + chatgptConsentDisclosure( + r.requestedScopes?.includes("paperclip:configure") === true, + r.consentWording, + r.configureDisclosed, + ) && exactAcceptedChatgptScopes( r.requestedScopes, r.acceptedScopes, @@ -224,3 +231,18 @@ export function exactAcceptedChatgptScopes( (!requested.includes("paperclip:configure") || configureDisclosed === true) ); } + +/** Broad actual write consent is supported; configuration acceptance also needs + * the selected person's explicit private acknowledgement of requested authority. */ +export function chatgptConsentDisclosure( + requestedConfigure: boolean, + wording: unknown, + acknowledged: unknown, +) { + return ( + typeof wording === "string" && + wording.trim().replace(/\s+/g, " ") === + "Write all of your Paperclip data" && + (!requestedConfigure || acknowledged === true) + ); +} diff --git a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.test.ts b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.test.ts index eb0c8d1280c..c5b527d392e 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.test.ts @@ -7,6 +7,7 @@ import { import { namedChatgptRequest, exactAcceptedChatgptScopes, + chatgptConsentDisclosure, validChatgptInput, validChatgptReceipt, createdChatgptAppId, @@ -79,6 +80,7 @@ function receipt(): ChatgptReceipt { requestedScopes: [...scopes], acceptedScopes: [...scopes], configureDisclosed: true, + consentWording: "Write all of your Paperclip data", }; } function valid(): ExternalObservation { @@ -308,6 +310,24 @@ describe("finite private named-provider contract", () => { ), ).toBe(false); }); + it("accepts actual broad write wording plus explicit requested configure acknowledgement", () => { + expect( + chatgptConsentDisclosure(true, "Write all of your Paperclip data", true), + ).toBe(true); + expect( + chatgptConsentDisclosure(true, "Write all of your Paperclip data", false), + ).toBe(false); + expect( + chatgptConsentDisclosure(true, "Read all of your Paperclip data", true), + ).toBe(false); + expect( + chatgptConsentDisclosure( + false, + "Write all of your Paperclip data", + false, + ), + ).toBe(true); + }); it("requires actual named official callback/person/company request", () => { const r = { id: "request", diff --git a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.ts b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.ts index 2ec84fb9e4d..ed2a4c760dd 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-setup.ts @@ -25,6 +25,7 @@ import { import { setupCheckComment, exactAcceptedChatgptScopes, + chatgptConsentDisclosure, type ChatgptReceipt, } from "./external-chatgpt-contract.js"; import { waitForReply } from "./identity-support.js"; @@ -112,7 +113,12 @@ export function gradeChatgptSetup(value: unknown) { r.realHumanConsent === true && r.actualProviderCallback === true && r.boundMcpComment === true && - r.zeroNativeAdmissions === true, + r.zeroNativeAdmissions === true && + chatgptConsentDisclosure( + r.requestedScopes?.includes("paperclip:configure") === true, + r.consentWording, + r.configureDisclosed, + ), "The selected human really uses ChatGPT's private custom-app UI and submits the actual Paperclip OAuth consent; an operator acknowledgement or generic MCP client alone is insufficient.", ); @@ -428,6 +434,8 @@ export const chatgptSetupCase = definition( actualNamedUi: true, actualHumanConsent: true, zeroNativeAdmissions: state.zeroNativeAdmissions, + consentWording: receipt.consentWording, + acceptedScopes: receipt.acceptedScopes, }); await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" }); }, diff --git a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-worker.ts b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-worker.ts index 809d645571b..c5074bcd8cb 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-chatgpt-worker.ts +++ b/tests/runner-e2e/chat-catalog-domains/external-chatgpt-worker.ts @@ -21,6 +21,7 @@ import { setupCheckComment, createdChatgptAppId, exactAcceptedChatgptScopes, + chatgptConsentDisclosure, type ChatgptInput, type ChatgptReceipt, } from "./external-chatgpt-contract.js"; @@ -320,14 +321,32 @@ async function main() { throw new Error( "Actual ChatGPT client/company/write request does not match qualification", ); - const configureDisclosed = - !request.requestedConfigure || - (await consent - .getByText(/configure agents, projects (?:and|&) skills/i) - .isVisible()); + const consentLabel = consent.getByText("Write all of your Paperclip data", { + exact: true, + }); + await consentLabel.waitFor({ state: "visible" }); + const consentWording = await consentLabel.innerText(); + await pane( + operator, + "Review and approve the actual Paperclip consent", + identity + + `

On the real Paperclip consent tab, select this exact company and explicitly approve read and write access for the single controlled qualification below. ${request.requestedConfigure ? "Configuration authority is also requested. The actual broad Write all of your Paperclip data choice includes configuring agents, projects and skills in this isolated company; explicitly acknowledge that authority here, then knowingly accept the real broad write choice on the actual consent tab." : "No configuration authority is requested or accepted."} Never approve agent/impersonation scope. Click Connect organization THERE, then finish creating the private ChatGPT app. This panel never approves consent.

`, + request.requestedConfigure + ? [ + "I explicitly accept the requested configuration authority for this isolated company under the real broad write consent choice.", + ] + : [], + "I reviewed the real consent tab", + ); + await confirm(operator, 120000); + const configureDisclosed = chatgptConsentDisclosure( + request.requestedConfigure === true, + consentWording, + true, + ); if (!configureDisclosed) throw new Error( - "Actual requested configure authority is not visibly disclosed on Paperclip consent", + "Actual broad write consent and explicit requested configuration acceptance were not established", ); const submitted = consent.waitForResponse( (r) => @@ -337,14 +356,6 @@ async function main() { r.request().method() === "POST", { timeout: 120000 }, ); - await pane( - operator, - "Review and approve the actual Paperclip consent", - identity + - `

On the real Paperclip consent tab, select this exact company and explicitly approve read and write access for the single controlled qualification below. ${request.requestedConfigure ? "Configuration authority is also requested: explicitly review the real visible Configure agents, projects and skills disclosure before accepting it for this isolated company." : "No configuration authority is requested or accepted."} Never approve agent/impersonation scope. Click Connect organization THERE, then finish creating the private ChatGPT app. This panel never approves consent.

`, - [], - "I reviewed the real consent tab", - ); await consent.bringToFront(); const response = await submitted, payload = response.request().postDataJSON() as { @@ -523,6 +534,7 @@ async function main() { ], acceptedScopes: [...grant.scopes], configureDisclosed, + consentWording, }; await writeFile( path.join(directory, "qualified.json"), diff --git a/tests/runner-e2e/chat-catalog-domains/external-fixtures.md b/tests/runner-e2e/chat-catalog-domains/external-fixtures.md index 3adf3bdad69..57d652f4c95 100644 --- a/tests/runner-e2e/chat-catalog-domains/external-fixtures.md +++ b/tests/runner-e2e/chat-catalog-domains/external-fixtures.md @@ -335,9 +335,13 @@ availability. After the unchanged canonical CEO query, that person actually uses ChatGPT's supported private custom MCP app UI and the real Paperclip consent tab. The child observes the actual named-client request, official ChatGPT callback, explicit company and scope acceptance, and one new saved grant. Configuration -scope is permitted only when requested, visibly disclosed in the real consent, -and explicitly accepted; agent authority is excluded. The current source's -write-only consent label does not itself establish configuration disclosure. +scope is permitted only when actually requested and explicitly accepted by the +authorized person under the real broad **Write all of your Paperclip data** +choice. The private prompt separately requires that person's explicit +acknowledgement of the requested configuration authority; it does not require +an invented separate picker or literal agents/projects/skills enumeration on +the product consent page. Actual visible consent wording and the exact scope +intersection are recorded; agent authority is excluded. Changed or missing controls and failed provider authorization are nonpassing observations, never grounds for a generic-client substitution.