Skip to content

feat: add native browser profile targeting across MCP and Claw - #2908

Merged
Nikhil (shadowfax92) merged 37 commits into
mainfrom
feat/med-profile-apis
Oct 14, 2026
Merged

Nikhil (shadowfax92) merged 37 commits into
mainfrom
feat/med-profile-apis

Conversation

@shadowfax92

@shadowfax92 Nikhil (shadowfax92) commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A task can select an existing regular browser profile once and retain that selection across tools, JavaScript runs and legitimate Claw session successors. Each request captures its binding and connection epoch; rebinding affects later requests. Unbound calls keep their existing behavior.

browser_profiles list/load -> native saved-profile catalog
browser_target set         -> logical task binding
request/run                -> captured BrowserView
                           -> tools, raw CDP, cookies/history, grouping

browser_profiles supports list and load. browser_target supports get, set, set_window, clear_window and clear; window CRUD stays separate. Saved profile handles are distinct from genuine live CDP contexts and Claw agent profiles. Unknown or missing profiles are not created implicitly. Working windows control placement; strict pins restrict access and fail when closed.

Claw retains intent through aliases and successors and requires explicit reselection after native handles expire. Groups are reused per conversation, saved profile and actual normal window. Explicit and deferred grouping share the operation guard through native readback. Clearing selection does not close borrowed windows or delete profiles.

Bound access includes normal windows only. OAuth and window.open popups return page_out_of_scope while bound; explicitly clear selection to drive them. UI implementation is deferred.

Current-main integration

Head 23e9be1f19858498757b4c4b1e47970b134412d2 contains both main b1c9bb10 and profile head 31ab08d5 without rewriting history. The sole textual conflict was in screenshot.rs: it retains main's uncapped native-density viewport and optional size axes while using the captured BrowserView for both metrics and capture. A regression proves out-of-scope refusal and rejection of target replacement during metrics before any capture. Its mutation check fails when the guarded page session is bypassed.

Local verification: 1,061 Rust workspace tests, strict all-target Clippy, formatting, TypeScript checks, and 43 screenshot/register checks passed. The invalid-size fixture now admits an existing page before asserting that no page protocol call is made, preserving central admission order. All 17 profile patches (including S1/S2) match 31ab08d5; main's 11 disjoint native patches are preserved, the eight diagnostics stay absent, and protocol SHA-256 remains a35addbd…606710. Fresh Tests (19 jobs), Code Quality (5 jobs), CLA, title validation and DeepScan all passed at this exact head. The three owner-deferred conversations are resolved, with their unvetted findings retained below.

Profile-loader simplification (S1 + S2)

The existing-only loader no longer changes Chromium's public ProfileManager::CreateProfileAsync. LoadExistingProfileByPath handles the startup and unloading deferrals, checks CanCreateProfileAtPath and the catalog entry, then creates and registers the existing-only Profile itself when it owns a new load. It then joins the stock CreateProfileAsync, which finds the registered entry and runs or queues the callback. Only the call that registered reports loaded:true; joining an in-flight or loaded profile reports false.

Profile::CreateProfile keeps upstream's switch. An existing-only preflight precedes it, the asynchronous case skips directory creation, and the synchronous case refuses to create a directory that vanished after the preflight. Upstream's profile.h comment and OnPrefsLoaded formatting are restored. The prefs-status check and the Windows cache guard are unchanged.

 LoadExistingProfileByPath(path, cb)      // after deferrals
-  CreateProfileAsync(path, join, {}, create_if_missing=false)   // changed public API
+  if new: CreateProfile(async, create_if_missing=false); RegisterOwnedProfile
+  CreateProfileAsync(path, join)                                 // stock API

Rebase surface in the profile family drops from 35 rewritten upstream lines and 23 zero-context hunks to 7 and 15. profile_manager.{cc,h} are now pure insertions. The whole feature goes from +836/−71 to +817/−43. Native commit 70a7f70a01c950b8300979aa497bd85974c13bbc (parent: reviewed profile tip 5d203e25) is extracted here as 31ab08d5. Four patch files changed; the other 13 profile patches and the remaining 390 store files are byte-identical. In that patch-only S1/S2 commit, the protocol (a35addbd…) and agent tree (2e4b96fa) were unchanged. The eight diagnostic paths remain absent.

Verification

  • autoninja -C out/Default_browseros_arm64 chrome passed at exact 70a7f70a (exit 0). GN/Ninja hashes and mtimes are unchanged, and a no-op re-run confirmed the target was current.
  • The linked library exports only the stock 3-argument CreateProfileAsync.
  • One independent pinned review (Codex, xhigh) came back clean on correctness, standards and spec.
  • Extraction: 17/17 MATCH and APPLIES, zero skipped; re-extraction is idempotent.
  • S1/S2 Tests CI passed at exact 31ab08d5 (manually dispatched because the patch-only push is path-filtered): 18 jobs succeeded, including Claw server, Claw MCP, Claw server quality and the aggregate gate; only the dispatch-inapplicable summary was skipped.

New targeted runtime on this macOS build (one healthy launch). The browser was launched directly from the reviewed output against a private fixture. A 1,054-file runtime closure was hashed identically before and after. There were no fatal or spawn lines, Crashpad helpers exited on their own, the leader exited 0, and nothing was left running.

  • Discovery and load: no window, catalog or directory growth; first load true, repeat and already-loaded false.
  • Two concurrent loads coalesce to exactly one true.
  • Joining a stock in-flight load returns false. The load was started by the profile picker's statistics loader, and an ordering fence proved ours was pending.
  • Unknown, stale, missing-directory, missing-Preferences and corrupt-Preferences loads fail and create no directory or Preferences.
  • Eng then Nik normal windows route cookies and History per profile; the unrouted default path still works.
  • MCP/run smoke: the unchanged 49576d57 standalone MCP host (same agent tree) bound eng, ran browser work in eng's window, then bound nik; later run/tabs calls used nik only.

Async removal race: partial. The race was staged for real: an ordering fence proved the load pending past the native preflight, and the fixture directory was removed before any reply. It was not recreated after the reply or after shutdown. The underlying profile load does not complete. Upstream JsonPrefStore marks the read no_dir and never completes, and the Preferences/Secure Preferences SegregatedPrefStore waits for both stores, so OnPrefsLoaded never runs. Native replies "Profile load timed out" at 30 s, and that saved handle stays "still pending" until restart.

The hang was observed on 70a7: after the 30 s native timeout, 180 s of subsequent observations still found the load pending, blocking that handle until browser restart. That it predates S1/S2 is a source-tracing inference, supported by the unchanged upstream prefs path and the older 8e837 timeout receipt; the previous candidate was not re-run for an experimental comparison. The owner accepts this documented limitation for this merge. It is reported, not fixed here. Also observed: failed loads of a profile with missing or corrupt Preferences leave stock tracked-prefs writing Secure Preferences into that existing directory.

Not staged at runtime: a stock caller joining our in-flight load, the startup and unloading deferral paths, and synchronous existing-only mode (no caller). CI's Linux browser is not acceptance of the macOS build.

Earlier follow-up: diagnostic removal

The renderer-termination diagnostics were removed in d3a8342e (eight content/browser patches plus the feature entry). That native removal 289b233e had a tree identical to 5d203e25. Its Tests run passed at that commit. At the diagnostic-removal and S1/S2 heads, the agent/Rust tree was identical to 49576d57, which passed 984 executable tests, Rustdoc, formatting and strict Clippy; those patch-only updates added no Rust changes. The path-filtered Code Quality workflow did not run for these Chromium-only deltas; its prior run covered that unchanged agent tree. Current-main integration is verified separately. Historical 8e837 receipts stay historical and are not runtime proof for this candidate.

Deferred external review

The owner explicitly deferred these three findings to TKT-1025, assigned to Nikhil Sonti (Todo, priority 1). They remain unvetted and unfixed. With explicit owner authorization, only their three blocking review conversations were resolved as accepted deferred follow-ups, preserving the original comments. This is not a fix, dismissal or vetting claim; no admin bypass was used.

Merge Danger

Door: two-way; no profile migration or new persisted profile database. Ship matching native and Rust components together.

Blast radius: native catalog/context routing, Rust admission and Claw task/group persistence. No owner live profile or installed application is used or replaced by these follow-ups. An existing-only load whose directory disappears mid-load stays pending until the native 30 s timeout and blocks that handle until restart (observed on 70a7; pre-existing status is source-tracing inference, not a previous-candidate rerun). A renderer destroyed while evaluation awaits a page promise can still leave that native command pending until the existing timeout. Commands without native expected-window/member preconditions retain a narrow physical-drag interval after the final observation.

The owner now authorizes merging after current-main conflict resolution and exact-head CI, then a scoped patch-version+1 and macOS arm64-only Neo test draft. No live browser/component feed promotion, stable aliases or announcements are selected. Canonical native refresh will follow the actual merge under a new exclusive refresh lease. All task branches, worktrees, panes and historical evidence remain retained; no force push or installed-app change.

🤖 Generated with Claude Code

Re-admit bound pages after advisory settling and dialog races before reading shared dialog state. Add a disposable protocol regression proving denied readbacks retain the signal without disclosing its text.
Check the actual eligible group window before applying inherited creation defaults in scripts and granular tools. Keep explicit conflicts in core admission and cover alternate working-window placement and moved defaults against the real guard.
Implement the approved CdpConnection epoch methods with generation checks and dispatch publication under one fixture lock. Cover decoded and raw futures prepared before reconnect without recording or mutating the replacement fixture. Validation awaits the published core trait addition; current errors are the exact missing epoch members.
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ Tests passed: 3381/3384

Ran 16 of 16 suites (0 not affected by this change).

Suite Passed Failed Skipped
✅ server-agent 216/216 0 0
✅ server-api 289/289 0 0
✅ server-tools 256/256 0 0
✅ server-browser 10/10 0 0
✅ server-integration 10/10 0 0
✅ server-lib 199/199 0 0
✅ server-root 38/41 0 3
✅ agent 372/372 0 0
✅ claw-app 502/502 0 0
✅ claw-onboard 100/100 0 0
✅ app-onboard 84/84 0 0
✅ build 26/26 0 0
✅ release 66/66 0 0
✅ claw-server 1061/1061 0 0
✅ claw-server-quality passed 0 0
✅ claw-mcp 152/152 0 0

passed = ran successfully but emits no JUnit counts (a lint/format gate).

View workflow run

github-actions Bot and others added 7 commits October 13, 2026 03:36
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
A delayed callback can move the last page out of an explicit group and
delete the ID returned by a successful create. Order group mutations with
conversation grouping in granular calls and script tool dispatch, and
refresh membership through the captured creation view after lookup.

Cover update/close in both orders and real run dispatch, external placement,
queued client cancellation and operator Stop, and read-only progress.

Validation: 672 Claw tests and Rustdoc pass; strict all-target Clippy and
owned formatting pass with private Cargo build and target directories.
An acknowledged rebind advances selection revision but must reuse a live
native group in the same conversation, saved profile and actual window.
Remove revision from the private placement key while retaining captured
BrowserView/creation guards and the lookup signature.

Cover both scope revisits and a late older-revision callback with a full
global inventory assertion. Correct the existing generation expectation
to match the frozen contract; keep distinct destination assertions.

Validation: 673 Claw tests and Rustdoc, strict all-target Clippy and owned
formatting pass using private Cargo build and target directories.
Extract the reviewed and chrome-built native range 5d203e252e72d9cc01e6871d2d21f303a3594a35..8e837bd21d767ac7a4833fdf7e56e3efe9186933 as eight cumulative BASE_COMMIT patches. Register their dedicated renderer-termination-diagnostics owner.

All eight patches byte-match and apply to the canonical base. Existing store bodies, ownership, Rust tree, and protocol bytes remain unchanged.
@shadowfax92
Nikhil (shadowfax92) marked this pull request as ready for review October 13, 2026 17:29
@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[High impact] Adds browser profile targeting across core browser and MCP layers.

Fix live Stop cancellation and delayed regrouping before merging.

Findings

  1. P1 Stop leaves work running ▶
  2. P1 Ungroup gets undone ▶
  3. P2 Group names stay stale ▶
Fix with agent prompt
### Issue 1
packages/browseros-agent/apps/claw-server/src/services/sessions/manager.rs:416-421
If writing `target-bindings.json` fails, such as when the disk is full, `targets.forget()` returns an error before the session is removed or its active calls are cancelled. The cockpit reports an error while the agent keeps changing browser pages.

Stop the live work even when saving the durable Stop marker fails, and report or retry that write separately.

### Issue 2
packages/browseros-agent/apps/claw-server/src/api/mcp/effects/tab_groups.rs:433-438
This check misses an explicit ungroup that finishes before the delayed callback's first page read. Open a tab in a named group, then ungroup it while automatic grouping is queued: both reads see `group_id == None`, so the callback adds the tab back to the saved group. If that group disappeared, it creates another one instead.

Record explicit ungroup intent under the shared guard so older callbacks cannot undo a successful ungroup.

### Issue 3
packages/browseros-agent/apps/claw-server/src/api/mcp/effects/tab_groups.rs:238-244
The new early return skips pending title retries for bound calls that do not create a page. It also affects unbound calls once their group has a recorded scope. If `name_session` saves a new title but its native update fails, later reads and actions no longer retry it. The group keeps its old name until another tab is opened or the task is renamed again.

Keep pending-title retries for existing-page calls, using the captured `BrowserView` to restrict which groups they touch.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds native saved-profile discovery and loading, plus task-owned browser selection across MCP and Claw.

  • Tasks can select a saved browser profile and choose a window.
  • Each request uses one captured view for browser work.
  • Claw keeps task selection and grouping across eligible session successors.

Acknowledged scope and limits from shadowfax92:

  • UI implementation is deferred.
  • Page evaluation awaiting a renderer promise may remain pending until the existing timeout.
  • Commands without native expected-window/member checks retain the stated final physical-drag interval.
  • Renderer diagnostics have no matching native raw-status-15 terminal observation; historical attribution remains unresolved.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Catalog["Native saved-profile catalog"] --> Management["browser_profiles list / load"]
  Selection["browser_target set / set_window"] --> Store["Task target store"]
  Store --> Capture["Capture BrowserView once per request"]
  Capture --> Tools["Granular tools"]
  Capture --> Script["run: direct tools and raw CDP"]
  Tools --> Admission["Check profile, normal window, page and connection"]
  Script --> Admission
  Admission --> Native["Native browser commands"]
  Native --> Receipt["Created-page receipt"]
  Receipt --> Grouping["Delayed grouping under conversation guard"]
  Store --> Successor["Task aliases and successors"]
Loading

Reviews (1) · Last reviewed commit: "feat(chromium): extract renderer termina..." · Reviewed by Greptile

Capture the current CSS viewport at native device density when size is omitted. Constrain only supplied size axes and retain the existing full-page path.
Remove the eight added renderer-termination diagnostic patches and only their dedicated feature ownership. The complete BrowserOS tree returns to profile-only 49576; Rust, protocol bytes and all 394 existing store bodies remain unchanged.

Native removal 289b233ea9b93e49f1452ebdb6ac27db0b70b2c7 restores the complete reviewed 5d tree, passed the single independent source review and a fresh exact chrome build. Reduced extraction is idempotent; all 17 profile patches MATCH/APPLIES, and all eight removed paths have empty cumulative BASE diffs and no owner.
LoadExistingProfileByPath now creates and registers the existing-only
Profile itself and joins through the stock CreateProfileAsync, so the
public CreateProfileAsync signature, its deferral rebinds, creation
branch and the CreateMultiProfileAsync call site return to upstream.
profile_manager.{cc,h} become pure insertions.

Profile::CreateProfile keeps upstream's switch: the existing-only
preflight precedes it, the asynchronous case skips directory creation
and the synchronous case refuses to create a directory that vanished
after the preflight. Upstream's profile.h comment and OnPrefsLoaded
formatting are restored.

Strict existing-only loading, the prefs-status check, the Windows
cache guard and first-loader versus joiner reporting are unchanged.
Cumulative patches extracted from native task commit 70a7f70a
(built with the chrome gate and independently reviewed).
Integrate main b1c9bb1 (viewport-native screenshot default with optional
size axes) with the profile-target branch 31ab08d (captured BrowserView
guards).

- screenshot.rs: keep the bound view's guarded page session for
  Page.getLayoutMetrics and pass main's optional size. ToolCtx no longer
  exposes the raw registry session, so main's unrestricted lookup cannot
  return.
- tests.rs: main's invalid-size test now uses an existing harness page.
  Central dispatch admits the primary page before a handler validates its
  arguments; the test still asserts that no Page or Emulation call follows.
Main's default viewport clip adds Page.getLayoutMetrics before capture.
Exercise it through a profile-bound BrowserView: both calls use the
admitted page session, an out-of-scope page is never measured or captured,
and a target replaced during measurement fails before capture.
@shadowfax92
Nikhil (shadowfax92) merged commit a1eb311 into main Oct 14, 2026
27 checks passed
Nikhil (shadowfax92) added a commit that referenced this pull request Oct 14, 2026
Add platform, macOS architecture and draft-only publication choices to the full Neo release workflow, preserving all/universal/alpha defaults. Retain exact-source component drafts without public feeds, latest aliases or notifications; finalize only selected browser assets. Deferred component attachments fail closed before public mutation.

Advance only BROWSEROS_PATCH from 2 to 3 after profile PR #2908. Native profile patches and the full agent/Rust tree are unchanged.

Validation: one independent workflow review and vetted fixes; 1270 bos_build tests (two existing skips), 66 TypeScript release tests, Ruff/actionlint, and exact-head GitHub Tests and Build System Tests passed.

This prepares a macOS arm64 GitHub draft test build; it does not claim a built or runtime-verified artifact. TKT-1025 findings and the accepted removal-race limitation remain unfixed.
Nikhil (shadowfax92) added a commit that referenced this pull request Oct 14, 2026
)

* feat(claw): gate native profile features on the BrowserOS version

claw-server ships by OTA ahead of the native browser, so a new server can drive a
browser that predates PR #2908's profile bundle. Add one native capability table in
browseros-core (ProfileTargeting needs BrowserOS 0.51.1.3) keyed on the browser's own
instance.browseros_version from the launch config, read leniently: missing, blank or
malformed identity is unknown and supports no gated feature.

- Hide browser_profiles/browser_target, the profile instructions, run's profile docs
  and its browser.profiles/browser.target exports when unsupported.
- Refuse tagged tools centrally in execute_tool (cached lists, direct calls, run
  tool:*), and table-listed raw CDP methods and (method, param) pairs, before the
  gated operation is sent. Stock browserContextId uses stay allowed.
- Run a saved binding unbound on an unsupported native, keep it persisted, and add one
  note to the outer result. Supported capture and live identity checks are unchanged.
- CLAW_NATIVE_BROWSEROS_VERSION is a logged developer assertion; dev/dogfood sidecars
  record only a signed bundle's 10000.-prefixed CFBundleVersion.

* fix(claw): gate typed tab creation with a context; clarify dev stamp trust

Review follow-up. BrowserView::new_page now requires profile targeting before
sending createTab with an explicit browserContextId, matching the raw guard; creation
without a context is unchanged. The dev/dogfood writers trust the release pipeline's
10000.-prefixed CFBundleVersion stamp, not a verified signature; say so plainly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant