Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified dist/webbrain-chrome-25.8.3.zip
Binary file not shown.
Binary file modified dist/webbrain-edge-25.8.3.zip
Binary file not shown.
Binary file modified dist/webbrain-firefox-25.8.3.zip
Binary file not shown.
70 changes: 70 additions & 0 deletions scripts/build-zip.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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`);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/chrome/src/agent/adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/<addon-slug>/versions/submit/<id>/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.`,
Expand Down
34 changes: 34 additions & 0 deletions src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '');
Expand Down Expand Up @@ -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 || {}),
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/src/agent/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
24 changes: 24 additions & 0 deletions src/chrome/src/content/accessibility-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,30 @@
} catch {}
}

// Native form controls can be wrapped by a <label> without using a
// matching `for` attribute. `el.labels` covers both that pattern and
// explicit label associations, so action results do not expose an
// unnamed checkbox even though the page visibly labels it.
try {
const labelText = Array.from(el.labels || [])
.map(label => (label.innerText || label.textContent || '').replace(/\s+/g, ' ').trim())
.find(Boolean);
if (labelText) {
return labelText.length > MAX_NAME_LEN
? labelText.substring(0, MAX_NAME_LEN) + '...'
: labelText;
}
const wrappingLabel = el.closest && el.closest('label');
const wrappingText = (wrappingLabel?.innerText || wrappingLabel?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
if (wrappingText) {
return wrappingText.length > MAX_NAME_LEN
? wrappingText.substring(0, MAX_NAME_LEN) + '...'
: wrappingText;
}
} catch {}

if (tag === 'input') {
const t = (el.getAttribute('type') || '').toLowerCase();
const valAttr = el.getAttribute('value');
Expand Down
77 changes: 76 additions & 1 deletion src/chrome/src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,70 @@
|| String(el?.innerText || '').trim().slice(0, 160);
}

function _axVisibleConfirmationSurfaces() {
const surfaces = [];
const seen = new Set();
const selectors = [
'[role="dialog"]',
'[role="alertdialog"]',
'[aria-modal="true"]',
'dialog[open]',
'.modal',
].join(',');
const visible = (el) => {
try {
if (!el?.isConnected) return false;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = el.getBoundingClientRect();
return rect.width >= 20 && rect.height >= 20;
} catch {
return false;
}
};
try {
for (const surface of document.querySelectorAll(selectors)) {
if (!visible(surface)) continue;
const id = String(surface.id || '').trim();
const role = String(surface.getAttribute('role') || surface.tagName || '').trim().toLowerCase();
const heading = surface.querySelector(
'[role="heading"],h1,h2,h3,h4,h5,h6,[aria-label]'
);
const title = String(
surface.getAttribute('aria-label')
|| heading?.innerText
|| heading?.textContent
|| ''
).replace(/\s+/g, ' ').trim().slice(0, 160);
const actions = [];
for (const control of surface.querySelectorAll('button,a,[role="button"]')) {
if (!visible(control)) continue;
const name = _axAccessibleName(control).replace(/\s+/g, ' ').trim();
if (name && !actions.includes(name)) actions.push(name.slice(0, 120));
if (actions.length >= 6) break;
}
const signature = id
? `id:${id}`
: `surface:${role}|${title}|${actions.join('|')}`.slice(0, 480);
if (!signature || seen.has(signature)) continue;
seen.add(signature);
surfaces.push({ signature, title, actions });
if (surfaces.length >= 8) break;
}
} catch {}
return surfaces;
}

function _axNewConfirmationSurface(before = []) {
const previous = new Set(
(Array.isArray(before) ? before : [])
.map(surface => String(surface?.signature || ''))
.filter(Boolean)
);
return _axVisibleConfirmationSurfaces()
.find(surface => !previous.has(surface.signature)) || null;
}

function _axCheckboxIdentity(el, refId = '') {
try {
const id = String(el?.id || '').trim();
Expand Down Expand Up @@ -4107,6 +4171,7 @@
}
const checkedBefore = !!el.checked;
const checkboxIdentity = _axCheckboxIdentity(el, ref_id);
const confirmationSurfacesBefore = _axVisibleConfirmationSurfaces();
const base = {
method: 'set_checked',
ref_id,
Expand Down Expand Up @@ -4164,13 +4229,17 @@
needsTrustedClick: true,
marker: marker || undefined,
trustedSelector: trustedSelector || undefined,
_confirmationSurfaces: confirmationSurfacesBefore,
...base,
};
}
dispatched = true;
el.click();
const checkedAfter = !!el.checked;
const success = checkedAfter === checked;
const confirmation = success
? null
: _axNewConfirmationSurface(confirmationSurfacesBefore);
return {
...base,
success,
Expand All @@ -4184,7 +4253,13 @@
desiredChecked: checked,
actualChecked: checkedAfter,
},
...(success ? {} : {
...(confirmation ? {
confirmationRequired: true,
recoveryRequired: 'confirmation_dialog',
observedEffects: ['confirmation_dialog_opened'],
confirmation,
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.',
} : success ? {} : {
noProgress: true,
error: `Checkbox remained ${checkedAfter ? 'checked' : 'unchecked'} after one synthetic click. This page may require trusted pointer input.`,
}),
Expand Down
4 changes: 3 additions & 1 deletion src/firefox/src/agent/adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -15745,7 +15745,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/<addon-slug>/versions/submit/<id>/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.`,
Expand Down
2 changes: 1 addition & 1 deletion src/firefox/src/agent/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
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. 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: {
Expand Down
Loading
Loading