Skip to content

Integrate Browser Operator 0.7.0 Chromium 158 release candidates into main - #111

Draft
tysonthomas9 wants to merge 33 commits into
mainfrom
codex/browser-operator-070-integration
Draft

tysonthomas9 wants to merge 33 commits into
mainfrom
codex/browser-operator-070-integration

Conversation

@tysonthomas9

@tysonthomas9 tysonthomas9 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

The bundled AI panel previously used a frontend GitHub release banner while the macOS browser had a separate native updater. Product 0.7.0 now shows “Check for updates” and opens Chromium's real native About/update page. Legacy clients retain their public latest-release migration notice.

This rollup integrates the reviewed stack onto main, preserves main's AI files, resolves duplicate settings/resources and captures the exact existing native branding recipe. Component reviews are linked in the release audit; PR #109 covers the update entry and PR #110 the branding coverage. No node_modules changes are introduced relative to main.

Both Chromium 158.0.8127.18 DMGs are verified locally:

  • ARM64 and universal: official/PGO2/full-typecheck builds, Developer ID signed, Apple notarized/stapled, Gatekeeper accepted.
  • Universal ARM64 and Intel x86_64 slices passed actual browser/renderer runtime tests; Intel ran under Rosetta.
  • The native update action works, six other privileged URL variants remain blocked, and the native updater completes authenticated production cloud checks.
  • Visible Guest tests passed toolbar open/close/reopen, product 0.7.0, native About status, model picker and settings. The installed updater registration, helper bytes and LaunchAgents were restored and verified.
  • 31 release-tool checks and five legacy migration notification checks passed. All ten native patches exactly reproduce the release input. Claude reviews and responses are in the comments.

Candidate source: 6926ab2. Provider conversations and physical Intel hardware remain untested. Production 159 install/upgrade qualification remains separate.

Publishing stays held until the owner merges this PR with the exact approved candidate tree. The merge gate validates that merge commit and its ancestry. No new 0.7.0 public/private release assets, release tags or Omaha offer have been published. Public 0.7.0 DMGs and private Omaha copies are staged for after merge; the existing private 158 release is unchanged.

Windows compilation is a separate, nonpublishing CircleCI candidate in tysonthomas9/test-github-pipeline#9. CircleCI accepted its workflow; Windows worker status is being verified.

The existing automated GitHub claude-review check currently fails before analysis with HTTP401 because the Claude Code GitHub App is not installed on this repository. This is separate from the completed Claude CLI reviews posted above. No app permissions were expanded and no required check was bypassed; the PR remains draft for owner review/configuration.

tysonthomas9 and others added 30 commits November 24, 2025 16:19
Backport 22f74fbeb2860d02b55f6481baf352006c158b9f and adapt c98961307b92b731acef5fee919d915eef4086d8 to the existing FontView lifecycle. Add regression coverage without changing AI chat or vendored dependencies.
Backport 22f74fbeb2860d02b55f6481baf352006c158b9f and adapt c98961307b92b731acef5fee919d915eef4086d8 to the existing FontView lifecycle. Add regression coverage without changing AI chat or vendored dependencies.
@tysonthomas9

Copy link
Copy Markdown
Collaborator Author

Claude reviewed the0.7.0 release changes with tools disabled. Review transcript and follow-up retained locally; publishing remains held until the owner merges PR111.

Confirmed findings addressed:

  • Patch assembly now ignores ambient Git environment/global/system config, force-tracks source inputs and emits a fixed binary diff format. A regression test covers hostile ignore/diff settings.
  • Production publishing always requires a recorded merge policy. Packaging requires frozen inputs. The merge gate checks the allowed owner and the complete frontend root tree, including dependencies, overlay and build scripts.
  • Baseline restore retries and records recovery evidence; checks helper bytes, Current target, app path/version and LaunchAgents; SIGTERM/HUP unwind the scope. Volatile update timestamps/cohort state are deliberately not rolled back.
  • Native-banner tests wait for host IPC/render, select the update control explicitly, check evaluation exceptions, and reject six other privileged URL variants.
  • Manual updater verification no longer assumes a native banner.

Clarifications: the always-visible native entry is intentional, and C++ allows only exact parsed chrome://help/ from the trusted bundled DevTools frontend. The earlier review did not include that native patch; the follow-up includes the full file. Release assets live in a dedicated repository, so its tag cannot point at a core repository commit; candidate source provenance is retained separately.

Tests after fixes:31 Python checks passed. All10 native patches reproduced current158 source exactly. Final signed DMG runtime checks are pending Keychain unlock; no0.7.0 release assets or catalog offer were published.

Follow-up review also addressed: unique restoration directory for every retry,
protect recovery from a second termination signal, tie the gate to the actual
PR111 merge commit and verify it remains on main, compare publication policy
and source tree to the verified frozen record, isolate final patch application
and source inspection, and use Lit.render(nothing) when hiding the banner.
Negative privileged-URL checks now observe a full3-second window rather than
accepting an initial transient blank page. Final frontend rebuild required.

Claude initial review (before fixes)

Release review: Browser Operator 0.7.0 / Chromium 158

This review is from the diff only. I did not run any tools. Several things this diff depends on are not in it: the native Chromium patches, VersionBanner.ts, chromium_build_input.py, verify_browser_updater.mjs, and the upload/publish step. Findings that rely on those are marked (unverified).


P1: fix before publishing

1. Patch helper can silently drop changes or produce a broken combined patch

scripts/apply_chromium_patches.py:23-40

The shadow repo uses your global git config. Two ways this goes wrong:

  • Global ignore rules drop files. If your global core.excludesFile matches a patched path (build/ is a common one, and Chromium patches often touch build/config/...), then git add --all (line 33) never tracks that file.
    • The patch still applies in the shadow tree.
    • But git diff (line 40) leaves out changes to untracked files.
    • The combined patch is then missing that change, and the --check against Chromium (line 60) still passes. Nothing reports the missing hunk.
  • Diff config changes the output. diff.noprefix, diff.mnemonicPrefix, color.diff=always or diff.external all change what git diff prints. The result then applies at the wrong -p level, or fails to parse.
  • Inherited GIT_DIR / GIT_WORK_TREE (for example from a hook or gclient) would point every cwd=shadow command at a different repository.

Fix:

  • Run the shadow git commands with a clean environment: drop all GIT_* variables, and set GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1.
  • Use git add --all --force.
  • Use git diff --no-ext-diff --no-color --src-prefix=a/ --dst-prefix=b/ --binary.
  • Assert that git diff --name-only equals the set of paths parsed from the patches, so a dropped file fails loudly.

2. The publication hold is not fail-closed

scripts/freeze_chromium_release.py:64-74, scripts/package_chromium_release.py:275-277, scripts/updater/release_merge_gate.py:13-33

The policy only reaches the manifest on one path, and nothing shown enforces it:

  • Freeze: it records publication_policy only if policy_path.exists(). If the file is renamed, or the version string in Version.ts changes, the gate disappears without any error.
  • Package, non-frozen path: provenance is None here (package_chromium_release.py:353-356). The policy is never recorded, so a 0.7.0 DMG built straight from out/Release has no hold at all.
  • Gate script: release_merge_gate.py:33 uses manifest['publication_policy']. A missing key gives a KeyError traceback, not a clear RELEASE HELD message.
  • Owner merge not checked: the hold says the owner must merge, but the gate only checks merged and the base branch. It never checks pr['merged_by']['login'].
  • (unverified) The upload step is not in this diff. Nothing shown makes publishing call require_merged_release.

Fix:

  • Have package look for config/releases/<browser_operator_version>.json itself, and refuse to package without a recorded policy whenever that file exists, on either path.
  • In the gate, treat a missing policy as held.
  • Add an allowed list of mergers to the policy and check merged_by against it.
  • Make the publish script call the gate unconditionally. It should publish exactly the returned release_commit, not whatever main points to later, because main can move between the check and the publish.

3. Baseline restore can leave your real updater pointed at a test copy

scripts/updater/macos_smoke_scope.py:75-84, scripts/package_chromium_release.py:232-254

This breaks the stated requirement that the existing updater profile is restored even when a test fails.

What happens on failure:

  • During the candidate smoke test, the user-level registration (prefs.json ecp) is moved to tests/installed/Browser Operator.app.
  • On failure, installed is kept on purpose, because line 254 is never reached.
  • If restore_baseline() then fails, the user's updater is left registered to a copy inside the verification folder. For example, verify_browser_updater.mjs 'successful-check' makes a real network update check, and a network glitch is enough to fail it.
  • There is one attempt, no retry, and no recovery instructions. The next run's require_clean() then refuses to start (line 67), so the damage is detected but never repaired.

Signals are not covered. __exit__ runs on exceptions and Ctrl-C (KeyboardInterrupt). It does not run on SIGTERM or SIGHUP, such as a closed terminal or a killed CI job. Python's default handler exits without unwinding.

"Restored exactly" checks less than it claims. baseline_registration() compares only ecp, pv and the helper hash.

  • In baseline mode, LaunchAgents and the rest of the app's prefs.json entry are never snapshotted (cohort, ap, brand, and so on).
  • If the candidate's same-version helper replaced Current, line 80 raises. But nothing can restore the original helper, so the user is left with the candidate's helper.

Also:

  • When the post-restore check itself fails, the raise at line 70 says "must have the same version and path", which hides the real cause: restore did not work.
  • A failing hdiutil detach in the finally at line 256 (the volume can still be busy) replaces the original test exception.

Fix:

  • Snapshot the full app registration dict, the LaunchAgents list and the Current target in __enter__. Compare all of them in __exit__.
  • Retry restore a bounded number of times.
  • On final failure, write native-updater-baseline-RESTORE-FAILED.json with the original values, print the manual recovery command, and keep the original exception as the primary error.
  • Install SIGTERM and SIGHUP handlers that raise, for example KeyboardInterrupt, so __exit__ runs.
  • Consider giving the candidate test its own registration that it never shares with the baseline, so restore cannot fail halfway.

P2: should fix

4. The merge-gate input set leaves out shipped product inputs

scripts/freeze_chromium_release.py:73, scripts/updater/release_merge_gate.py:22

Only these top-level paths are compared: front_end, BUILD.gn, config, scripts, patches.

A merge that only changes inspector_overlay/, root third_party/ (the esbuild/TypeScript toolchain), package.json/package-lock.json, DEPS, .gn or build_overrides/ would pass, even though those change the shipped frontend.

Fix: compare the root tree SHA (HEAD^{tree}). Or allow-list the paths that may differ (docs, .github) and require everything else to match.

5. Native-updater banner check has no wait and will be flaky

scripts/verify_chromium_release.mjs:101-117

#checkForUpdates is async and makes a getHostConfig round trip before the banner renders. The banner lookup runs once, right after the accessibility check. If the banner is not there yet, native is undefined and the release fails.

Fix: poll for nativeUpdater === true the same way the other loops in this file do.

The click step (lines 119-127) also never checks exceptionDetails:

  • A missing banner shows up later as the less useful "must open the real native About/update page".
  • It clicks the first button in the shadow root. Select the "Check for updates" control explicitly instead.

6. --manual-updates with frozen input will always fail verification

scripts/package_chromium_release.py:250-251

product_version is passed whenever provenance has it, whatever updater_enabled is. The .mjs then requires the native updater banner, which a manual-update build does not show.

Fix: pass it only when updater_enabled. Or make it an explicit error if 0.7.0 manual builds are meant to be unsupported.

7. Privileged URL check: native allow-list needs exact matching (native patch unverified)

front_end/panels/ai_chat/ui/ChatView.ts:1283-1284, scripts/verify_chromium_release.mjs:136-146

The frontend opens chrome://help/, and the test expects the target to show up as chrome://settings/help after the redirect. That works.

The risk is in the native OpenInNewTab exception:

  • It should be an exact match on the parsed, canonical GURL("chrome://help/"), with no query, fragment or extra path.
  • A host-only or prefix match would let chrome://help/<anything> through, along with whatever the help → settings rewrite does with extra path parts.

The test checks one negative case only (chrome://settings/passwords). Add:

  • chrome://help/x
  • chrome://help/?a
  • chrome://help#x
  • CHROME://HELP/
  • chrome://settings/help (only the alias should be allowed)
  • chrome-untrusted://…

Also confirm the exception applies only to the bundled devtools:// frontend, not to a --custom-devtools-frontend origin.


P3: low / nits

  1. Asset and branding copies follow symlinks at the destination. apply_chromium_patches.py:63-66, 90-97.
    • read_bytes()/copyfile follow a destination symlink, or a symlinked parent folder, out of the Chromium tree.
    • The shadow check at line 27 also checks only the final path component, not parent folders. git apply protects the patch itself, but not these copies.
    • Fix: reject the copy if destination.resolve() is not under chromium.resolve(), or if any path component is a symlink.
    • Separately, the apply step is not atomic: if asset copying fails after git apply -, the tree is left partly patched.
  2. Status message misses the no-apply case. apply_chromium_patches.py:98: "Validated N pinned branding assets" prints only with --apply, but the check runs in both modes.
  3. VERSION parsing has no '=' in line guard. freeze_chromium_release.py:37, unlike apply_chromium_patches.py:51. A blank or comment line would crash it.
  4. Merge accepts the standalone Util without per-input checks. merge_chromium_macos.py:39-50.
    • product + 'Util' is never checked as arm64/x86_64 before merging; only the merged result is (the same gap exists in freeze lines 40-41).
    • not a.get('updater_trust') (line 21) rejects test configs if their trust value is legitimately empty. Check this is intended.
    • The kept host tools are arm64 only, but package's Rosetta preflight (package_chromium_release.py:359-362) also passes on an Intel Mac. A universal build packaged there would fail later at dmg_tool. Add an arm64 host check.
  5. Banner settings and host config. ChatView.ts:1230-1239, 1281.
    • The native banner ignores #isVersionBannerDismissed and #messages.length > 1, so it shows on every render with no way to dismiss it. Confirm that is intended.
    • Root.Runtime.hostConfig is already filled in at startup. Reading it directly avoids an IPC round trip per check, and avoids hanging if a host never calls the callback.
  6. Debugging-port race. package_chromium_release.py:136-138: the free-port check binds and closes the port before launch. Another process could take it in between. This is low risk in a dedicated qualification session.

Checked, no issues found

  • BUILD.gn metadata: taking release JS from get_target_outputs(":ai_chat") removes the drift-prone duplicate list. This is correct as long as gn gen accepts it, and your successful builds suggest it does. The new core/root, core/platform and core/host deps match the new imports.
  • Architecture merge: version, source, trust and settings are compared, and signing resources are checked byte for byte. The only allowed difference is the documented host-tool exception, and the Mach-O checks on it are sound.
  • Branding manifest: pinned revision, SHA-256 per file, and paths limited to chrome/app/theme/.
  • Package flow:
    • The baseline is checked for same version, team ID and bundle ID before use.
    • The smoke test runs on a disposable installed copy, never on /Applications.
    • Existing output is never overwritten.
Claude follow-up review (remaining findings addressed as described above)

Follow-up review

The fixes you listed hold up as described. These issues remain.

Code bugs

1. Baseline restore retries never run again. In package_chromium_release.py, verify_release.restore_baseline calls (tests / 'restore-installed-baseline').mkdir(). On attempts 2 and 3 this raises FileExistsError. The scope catches that, logs it in errors, and checks the registration again without relaunching the baseline. Use a separate directory per attempt (for example with tempfile.mkdtemp(prefix='restore-', dir=tests)), or exist_ok=True.

2. A second signal during restore aborts it and skips the failure record. In macos_smoke_scope.py, the interrupted handlers for SIGTERM and SIGHUP stay installed until the finally at the end of __exit__. SIGINT also stays at its default. A second signal while restore is running raises KeyboardInterrupt, which except Exception does not catch. This leaves restore half done and never writes native-updater-baseline-RESTORE-FAILED.json. Fix: at the top of __exit__, swap in a handler that only records the signal, or use SIG_IGN, for SIGTERM, SIGHUP and SIGINT. Re-raise after the evidence is written.

3. The merge gate does not tie the candidate tree to PR #111. require_merged_release checks two separate things: PR #111 was merged by an allowed owner, and the current main tip tree equals the candidate tree. It never checks that the tip is that merge. This has two effects:

Fix: compare the candidate against pr['merge_commit_sha']'s tree. Then confirm that commit is on the base branch: repos/{repo}/compare/{merge_sha}...{base} should report identical or ahead.

4. Gate inputs come from the release manifest, not the verified frozen input. In publish_test_update.py, production already runs load(a.build_input) and checks its hash. It then passes release.get('publication_policy') and release.get('release_source_trees') to the gate. Those come from release-manifest.json, which nothing ties to the frozen record. Fix:

  • Pass frozen['publication_policy'] and frozen['release_source_trees'], or assert they equal the release's copies.
  • Assert that policy['product_version'] == release['browser_operator_version'] and policy['chromium_version'] == release['version'].

5. Git isolation only covers combined_patch. Several other Git calls still read the user's and repo's config:

  • Final apply (apply_chromium_patches.py, main). The git apply --check - and git apply - calls in the Chromium tree get no env. The repo's local .git/config is read even with your isolation env. With apply.whitespace=fix set, the applied hunks can differ from the combined patch you validated. Pass env=environment, and add --whitespace=nowarn (a command-line flag overrides local config).
  • Freeze cleanliness check (source_state in chromium_build_input.py). It runs git diff --binary HEAD with the user's config. A diff.external driver or a textconv can change the output, or blank it, so a dirty tree looks clean. Add --no-ext-diff --no-textconv. Use the same isolated env there, in the rev-parse calls in freeze_chromium_release.py, and in the branding rev-parse.
  • Simplest fix: move the env builder into a shared helper.

6. The version banner can render invisibly (latent). VersionBanner.#render clears content with this.#shadow.innerHTML = ''. That deletes Lit's marker nodes, but Lit keeps its cached part on the shadow root. Any later Lit.render into the same root then writes into detached nodes and shows nothing. For example: an update banner shows, then info is cleared or dismissed set, then nativeUpdater becomes true. Fix: use Lit.render(Lit.nothing, this.#shadow, {host: this}) instead of setting innerHTML.

7. The privileged-URL negative test can pass falsely (test robustness). In verify_chromium_release.mjs, the chrome://settings/passwords check stops at the first new page target that has any URL. A tab that is about to commit passwords may briefly report about:blank, so the assertion passes anyway. Fix: poll for a fixed window, fail if any new target ever shows chrome://settings/passwords, and only then assert the final URL is about:blank.

Design suggestions

  • Most policy fields in config/releases/0.7.0.json are not enforced. The publisher ignores tag, make_latest, prerelease, both repository fields, publish_before_merge and activate_omaha_offer. Its actual behaviour differs: macos-* tags, browser-operator-releases, make_latest=false. Either check the fields that apply to this step, or remove or rename them so readers don't assume they're honoured.
  • allowed_mergers lives in the tree being gated. Anyone who can merge to main can add themselves to it. That is acceptable if branch protection is the real control. Otherwise, pin the list outside the release tree.
  • The frontend cleanliness check ignores nested dependencies. It looks only at the top-level diff and untracked files. A nested Git dependency whose checkout differs from its gitlink is recorded in dependency_source_states, but not checked. This is low risk inside a Chromium checkout, but a short check would make the "identical tree" claim exact.

@tysonthomas9

Copy link
Copy Markdown
Collaborator Author

Final ARM DMG qualification caught a functional failure: the AI update button reached DevToolsWindow's second inspected-page security check, which correctly replaced the privileged URL with about:blank. The failed candidate was not published; its permanent updater baseline was restored and its native cloud check succeeded.

The integration now invokes Chromium's fixed ShowAboutChrome command after the exact chrome://help/ and trusted bundled-origin checks. It never forwards arbitrary URLs to that command. Both ordinary DevTools navigation checks remain unchanged. Incognito uses the original profile, Guest retains its supported browser profile, system profiles are excluded, and a closing/unavailable browser is handled safely.

Claude separately reviewed this fix and found no blocking security issue. I verified the pinned source preserves untrusted-opener and reload restrictions. The suggested old debug_devtools argument does not occur in this implementation. The review's example equating settings/help to the help alias was inaccurate: the code compares only chrome://help/, and negative tests reject direct chrome://settings/help.

Both builds and final signing/DMG runtime requalification are in progress. Publication remains held until owner merge; Windows CircleCI starts after Mac qualification.

@tysonthomas9

Copy link
Copy Markdown
Collaborator Author

The corrected product0.7.0 Mac candidates are now fully qualified locally, with publication held:

  • Chromium158.0.8127.18 ARM64 and universal: Developer ID signed, Apple notarized/stapled, Gatekeeper accepted.
  • Both universal slices passed actual browser/renderer runtime checks (ARM64 native, Intel x86_64 under Rosetta).
  • The AI panel shows0.7.0, its native action opens chrome://settings/help, and six other privileged URL variants stay blocked. The real production native updater completed authenticated cloud checks.
  • Visible Guest-mode verification passed: toolbar open/close/reopen,0.7.0 update entry and actual native About status, model picker and settings. Settings were cancelled without saving. Permanent installed app/helper/registration/LaunchAgents restored and verified.
    -31 release-tool checks and5 legacy0.7 migration notification checks pass; all10 native patches exactly reproduce the input source. No node_modules changes relative to main.

Candidate source:6926ab27d98bc2d158e02a0ca47e0708d040a16b. ARM DMG SHA256:20026458190c81bab3012a1d9f7cf49e0f199c61766983ee5c0b83070f67f7c0. Universal:14ed927e27fd45587a0922624fe51d10c2b204a0e5e4e89d02481a0a1a664677.

Neither0.7.0 DMGs/private copies nor a native159 catalog offer have been published. Public latest0.7.0 migration assets and private Omaha copies are staged locally for after owner merge; existing private158 assets remain unchanged. Physical Intel hardware and provider conversations are not tested. Production159 upgrade qualification remains separate.

Windows CircleCI is tracked separately in tysonthomas9/test-github-pipeline#9. Its accepted workflow is https://app.circleci.com/workflow/03c7f9c2-c608-4b55-98c3-64d744c95c9d ; actual Windows job state is being checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant