diff --git a/dist/webbrain-chrome-25.8.3.zip b/dist/webbrain-chrome-25.8.3.zip index 7a6c9f171..7380233b6 100644 Binary files a/dist/webbrain-chrome-25.8.3.zip and b/dist/webbrain-chrome-25.8.3.zip differ diff --git a/dist/webbrain-edge-25.8.3.zip b/dist/webbrain-edge-25.8.3.zip index 7a6c9f171..7380233b6 100644 Binary files a/dist/webbrain-edge-25.8.3.zip and b/dist/webbrain-edge-25.8.3.zip differ diff --git a/dist/webbrain-firefox-25.8.3.zip b/dist/webbrain-firefox-25.8.3.zip index ab24aea1a..ef1d115c1 100644 Binary files a/dist/webbrain-firefox-25.8.3.zip and b/dist/webbrain-firefox-25.8.3.zip differ diff --git a/scripts/build-zip.mjs b/scripts/build-zip.mjs index bb0ec5436..2f91a4424 100644 --- a/scripts/build-zip.mjs +++ b/scripts/build-zip.mjs @@ -46,6 +46,56 @@ export function assertMatchingArchiveVersion(expected, actual, label) { } } +const FLAG_LICENSE_PATH = 'icons/flags/LICENSE.flag-icons.txt'; +const REJECTED_FLAG_LICENSE_PATH = 'icons/flags/LICENSE.flag-icons'; + +export function listZipEntryNames(filePath) { + const archive = readFileSync(filePath); + const eocdSignature = 0x06054b50; + const centralHeaderSignature = 0x02014b50; + const earliestEocd = Math.max(0, archive.length - 0xffff - 22); + let eocdOffset = -1; + for (let offset = archive.length - 22; offset >= earliestEocd; offset -= 1) { + if ( + archive.readUInt32LE(offset) === eocdSignature + && offset + 22 + archive.readUInt16LE(offset + 20) === archive.length + ) { + eocdOffset = offset; + break; + } + } + if (eocdOffset < 0) throw new Error(`${filePath} has no ZIP end-of-central-directory record.`); + + const entryCount = archive.readUInt16LE(eocdOffset + 10); + let offset = archive.readUInt32LE(eocdOffset + 16); + const entries = []; + for (let index = 0; index < entryCount; index += 1) { + if (offset + 46 > archive.length || archive.readUInt32LE(offset) !== centralHeaderSignature) { + throw new Error(`${filePath} has an invalid ZIP central-directory entry at index ${index}.`); + } + const nameLength = archive.readUInt16LE(offset + 28); + const extraLength = archive.readUInt16LE(offset + 30); + const commentLength = archive.readUInt16LE(offset + 32); + const nameStart = offset + 46; + const nameEnd = nameStart + nameLength; + if (nameEnd > archive.length) { + throw new Error(`${filePath} has a truncated ZIP filename at index ${index}.`); + } + entries.push(archive.toString('utf8', nameStart, nameEnd)); + offset = nameEnd + extraLength + commentLength; + } + return entries; +} + +export function assertStoreSafeFlagLicenseEntries(entries, label) { + if (!entries.includes(FLAG_LICENSE_PATH)) { + throw new Error(`${label} is missing ${FLAG_LICENSE_PATH}.`); + } + if (entries.includes(REJECTED_FLAG_LICENSE_PATH)) { + throw new Error(`${label} still contains Opera-rejected ${REJECTED_FLAG_LICENSE_PATH}.`); + } +} + function readJsonAtHead(relativePath) { const json = execFileSync('git', ['show', `HEAD:${relativePath}`], { cwd: root, @@ -55,6 +105,18 @@ function readJsonAtHead(relativePath) { return JSON.parse(json); } +function listTreeEntryNamesAtHead(relativePath) { + return execFileSync( + 'git', + ['ls-tree', '-r', '--name-only', `HEAD:${relativePath}`], + { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + } + ).split(/\r?\n/).filter(Boolean); +} + function runCli() { const workingPackage = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')); const headPackage = readJsonAtHead('package.json'); @@ -68,6 +130,10 @@ function runCli() { for (const { sourceDir } of targets) { const manifest = readJsonAtHead(`src/${sourceDir}/manifest.json`); assertMatchingArchiveVersion(version, manifest.version, `HEAD src/${sourceDir}/manifest.json version`); + assertStoreSafeFlagLicenseEntries( + listTreeEntryNamesAtHead(`src/${sourceDir}`), + `HEAD src/${sourceDir}` + ); } const distDir = path.join(root, 'dist'); @@ -83,6 +149,10 @@ function runCli() { ['archive', '--format=zip', '-o', out, `HEAD:src/${sourceDir}`], { stdio: 'inherit', cwd: root } ); + assertStoreSafeFlagLicenseEntries( + listZipEntryNames(out), + `dist/webbrain-${packageName}-${version}.zip` + ); console.log(` ✓ dist/webbrain-${packageName}-${version}.zip`); } } diff --git a/src/chrome/icons/flags/LICENSE.flag-icons b/src/chrome/icons/flags/LICENSE.flag-icons.txt similarity index 100% rename from src/chrome/icons/flags/LICENSE.flag-icons rename to src/chrome/icons/flags/LICENSE.flag-icons.txt diff --git a/src/chrome/src/agent/adapters.js b/src/chrome/src/agent/adapters.js index 42af95c7f..86edbfe1f 100644 --- a/src/chrome/src/agent/adapters.js +++ b/src/chrome/src/agent/adapters.js @@ -15746,7 +15746,9 @@ const ADAPTERS = [ - For an existing add-on, use its "Upload New Version" flow. Do not start from "Submit a New Add-on"; the existing manifest ID will fail validation as a duplicate add-on ID. - On the "Describe Version" step, both "Release Notes" (changelog) and "Notes to Reviewer" are optional. Leave both empty unless the user explicitly asks to provide text; do not generate or paste content merely because the fields are present. - If those textareas must be inspected, trust their DOM field names over visual order or a possibly misleading label: \`release_notes...\` is the public release-notes field and \`approval_notes\` is the reviewer-notes field. Call \`verify_form\` before final submission. -- On AMO compatibility checkboxes, use the tree's native \`checked=true|false\` state and \`set_checked({ref_id, checked:true})\`; never toggle with repeated \`click_ax\`. After a validation-rejected submit, call \`verify_form\` exactly once. If the intended checkbox is still unchecked, go directly to \`set_checked\` and submit only after \`checkedAfter:true\`. +- AMO compatibility checkboxes are optional platform choices, not validation acknowledgements. Preserve their current state unless the user asked to change platform support. Use the tree's native \`checked=true|false\` state and \`set_checked\`; never toggle with repeated \`click_ax\`. +- Selecting Firefox for Android opens a confirmation dialog and the checkbox remains unchecked until "Yes, I’ve tested my extension with Firefox for Android" is submitted. Never make that attestation without user-provided evidence. If the user has not said it was tested, choose "No, I have not tested" or leave Android unchecked, then continue. If \`set_checked\` returns \`confirmationRequired:true\`, handle the visible dialog once instead of retrying the checkbox. +- After a validation-rejected submit, call \`verify_form\` exactly once. Change only the checkbox that the validation result actually requires, and submit only after its verified state matches the intended choice. - On a URL matching \`/addon//versions/submit//source\`, answer "No" to "Do You Need to Submit Source Code?" Select the radio labeled "No", then verify the page says "You do not need to submit Source Code" before continuing. - After navigation or scrolling, re-read the accessibility tree and use the current ref_id. Never reuse or guess a ref_id from an earlier page state. - Continue past package validation only when it reports no errors. Read any rejection-related warnings, then use the existing version-submission flow rather than restarting as a new add-on.`, diff --git a/src/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js index a5430ded4..22621d947 100644 --- a/src/chrome/src/agent/agent.js +++ b/src/chrome/src/agent/agent.js @@ -12888,6 +12888,9 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d if (!response || response.success !== true || response.needsTrustedClick !== true) { return response; } + const confirmationSurfacesBefore = Array.isArray(response._confirmationSurfaces) + ? response._confirmationSurfaces + : []; const beforeDocument = await this._getDevDocumentIdentity(tabId); const trustedSelector = String(response.trustedSelector || ''); const marker = String(response.marker || ''); @@ -12974,6 +12977,17 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d const stateMatches = !checkboxIdentityMismatch && checkedAfter === args.checked; const success = trustedClickSucceeded && (stateMatches || verificationLostAfterTrustedClick); const checkboxIdentity = expectedCheckboxIdentity || observedCheckboxIdentity; + const confirmationSurfacesAfter = Array.isArray(verified?._confirmationSurfaces) + ? verified._confirmationSurfaces + : []; + const priorConfirmationSignatures = new Set( + confirmationSurfacesBefore + .map(surface => String(surface?.signature || '')) + .filter(Boolean) + ); + const confirmation = confirmationSurfacesAfter.find( + surface => !priorConfirmationSignatures.has(String(surface?.signature || '')) + ) || null; const completed = { ...response, ...(verified || {}), @@ -13008,6 +13022,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d marker: undefined, trustedSelector: undefined, }; + delete completed._confirmationSurfaces; if (verificationLostAfterTrustedClick) { return { ...completed, @@ -13021,6 +13036,25 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d warning: 'The trusted checkbox click was sent, but its document disappeared before the checked state could be re-read, likely because the click navigated or reloaded the page. Do not repeat set_checked; observe the current page.', }; } + if ( + trustedClickSucceeded + && !stateMatches + && !checkboxIdentityMismatch + && confirmation + ) { + return { + ...completed, + success: false, + verified: false, + confirmationRequired: true, + recoveryRequired: 'confirmation_dialog', + observedEffects: ['confirmation_dialog_opened'], + confirmation, + noProgress: undefined, + error: undefined, + warning: 'A confirmation dialog opened before the checkbox could reach the requested state. Do not call set_checked again. Re-read the visible accessibility tree and choose a dialog action only when it is supported by the user request or current evidence.', + }; + } const failureError = clickError ? `Trusted checkbox click did not complete: ${clickError.message || clickError}` : checkboxIdentityMismatch diff --git a/src/chrome/src/agent/tools.js b/src/chrome/src/agent/tools.js index 86091c5dc..2610752be 100644 --- a/src/chrome/src/agent/tools.js +++ b/src/chrome/src/agent/tools.js @@ -49,7 +49,7 @@ export const AGENT_TOOLS = [ type: 'function', function: { name: 'set_checked', - description: 'Idempotently set a native checkbox to the requested checked state by ref_id. Unlike click_ax, this never blindly toggles: it first reads checkedBefore, does nothing when already correct, performs at most one click when needed, and returns checkedAfter. On Chrome the state-changing click uses trusted selector-backed pointer input.', + description: 'Idempotently set a native checkbox to the requested checked state by ref_id. Unlike click_ax, this never blindly toggles: it first reads checkedBefore, does nothing when already correct, performs at most one click when needed, and returns checkedAfter. On Chrome the state-changing click uses trusted selector-backed pointer input. If the click opens a confirmation dialog instead of changing state, returns confirmationRequired:true; handle that dialog from fresh page evidence and do not retry the checkbox.', parameters: { type: 'object', properties: { diff --git a/src/chrome/src/content/accessibility-tree.js b/src/chrome/src/content/accessibility-tree.js index e96143ee9..968ddf06c 100644 --- a/src/chrome/src/content/accessibility-tree.js +++ b/src/chrome/src/content/accessibility-tree.js @@ -319,6 +319,30 @@ } catch {} } + // Native form controls can be wrapped by a