Repository navigation
feat(claw): use the original Default profile when a task has no target - #2919
Conversation
On a native with profile targeting, a task with no saved browser_target now runs in the browser's original Chromium profile (catalog directory "Default", never its display name), instead of whatever window was last focused. An explicit target still wins, and clearing it returns later calls to Default. - BrowserView::capture resolves the default from the live catalog on every capture, loads that existing profile if needed (opens no window), and reuses the scoped execution path, so tools, run primitives, tool:* and raw CDP share one decision. The effective binding is never persisted (revision 0) and its handles stay fresh. - With no Default window, opening a page creates one inactive normal Default window with the page as its first tab. An explicit target without a window keeps no_profile_window. - A missing, ineligible or unloadable Default gives default_profile_unavailable with no fallback to another profile. Management still works; an epoch change fails the admitted default view closed. - browser_target get reports the Default in use when nothing is selected. Profile guidance and the run, tabs and windows notes say so. - Natives without profile targeting keep the PR #2918 unbound path unchanged.
- A failed Default capture never widens to global routing. The Browser facade and the scoped event stream take the unbound path only for a genuinely unbound view. - With no Default window, open an inactive normal Default window and create the page in it with Browser.createTab, so the page is identified by its own creation reply. Raw createTab keeps every caller argument and only gets the window. - Profile management (browser_profiles, browser_target) is admitted through BrowserView::capture_for_management and never loads an unloaded Default.
[Medium impact] Changes how browser profile targeting works when no explicit selection exists. The PR appears safe to merge, with a non-blocking issue that opens extra windows during repeated page creation. Findings
Fix with agent prompt### Issue 1
packages/browseros-agent/crates/browseros-core/src/target.rs:1050-1052
When Default starts without a normal window, two sequential `browser.open` calls in one `run` each open a separate window. The first creation never updates `placement_window`, so both the typed and raw creation paths keep treating Default as windowless. This leaves users with extra windows instead of tabs sharing the first one.
Reuse the first automatically created window within the captured view, and add a test that opens two pages through that same view.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryUntargeted calls on capable browsers now use the original Default profile instead of the last-focused profile.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Capture call] --> B{Profile targeting supported?}
B -->|No| C[Legacy unbound routing]
B -->|Yes| D{Saved target?}
D -->|Yes| E[Capture explicit profile]
D -->|No| F[Find directory Default]
F --> G{Management call?}
G -->|Yes| H[Capture without loading]
G -->|No| I[Load existing profile if needed]
I --> J{Capture succeeds?}
J -->|No| K[Refuse ordinary work]
J -->|Yes| L[Scoped Default work]
L --> M{Page creation lacks a window?}
M -->|Yes| N[Create Default window]
M -->|No| O[Use captured window]
N --> P[Create tab]
O --> P
Reviews (1) · Last reviewed commit: "fix(claw): harden default-profile captur..." · Reviewed by Greptile |
✅ Tests passed: 1256/1256Ran 3 of 16 suites (13 not affected by this change).
|
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 23 changed function(s) (23 not verified).
Formal verification. PR-changed functions: 0/23 verified (0 proven, 0 may-equivalent, 0 distinguished) · 23 not verified (23 unsupported).
Not verified on this run: profile\_guidance\_is\_offered\_only\_to\_capable\_natives (unsupported), call\_tool (unsupported), get\_active\_page\_for\_window (unsupported), new\_page (unsupported), capture (unsupported), capture\_scope (unsupported), check\_epoch (unsupported), epoch\_guard (unsupported), new\_page (unsupported), status (unsupported), and 13 more.
Graphify review — findings
Adds a default-profile rule for tasks with no browser_target selection: calls run in the browser's original Default profile (directory "Default"), not the focused window. A selection made after admission doesn't move the admitted run or its detached grouping, and the default is never saved for the task. Unbound calls now resolve the default profile at admission, so a missing Default profile fails admission with default_profile_unavailable instead of falling back to the old unbound routing, and an explicit window new_page now resolves default scope before it honors window_id. Profile-management tools now capture their view through capture_for_management, so admission never loads the Default profile as a side effect; if the default capture fails, target_source still reports Default while binding() is None. The MCP instructions teach the default rule and action="clear" returning the task to Default, only for natives that support profile targeting, and older natives see no mention of it.
Worth a look
- Unbound calls now fail admission instead of using unbound routing —
packages/browseros-agent/crates/browseros-core/src/target.rs:300· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Explicit window new_page now resolves default scope before honoring window_id —
packages/browseros-agent/crates/browseros-core/src/browser.rs:110· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 812 functions depend on the 718 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
execute_with_cancellation()— 9 callers, 6 callees
Verification — 812 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 812 function(s) in the blast radius were not formally verified this run
Risky patterns (advisory)
- medium
no-unbounded-fetchinpackages/browseros-agent/crates/browseros-core/src/target.rs:319: unbounded read (no limit/bound): .read()
Docs that may be stale (advisory)
packages/browseros-agent/packages/agent-mcp-manager/README.md§ What each bucket means (lines 180-185): references changed symbolsgone
Formal verification
Could not verify: Could not verify profile\_guidance\_is\_offered\_only\_to\_capable\_natives.
The verifier did not have enough to check profile\_guidance\_is\_offered\_only\_to\_capable\_natives, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify call\_tool.
The verifier did not have enough to check call\_tool, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify get\_active\_page\_for\_window.
The verifier did not have enough to check get\_active\_page\_for\_window, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify new\_page.
The verifier did not have enough to check new\_page, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify capture.
The verifier did not have enough to check capture, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Self
Could not verify: Could not verify capture\_scope.
The verifier did not have enough to check capture\_scope, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify check\_epoch.
The verifier did not have enough to check check\_epoch, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify epoch\_guard.
The verifier did not have enough to check epoch\_guard, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify new\_page.
The verifier did not have enough to check new\_page, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify status.
The verifier did not have enough to check status, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify unbound.
The verifier did not have enough to check unbound, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Self
Could not verify: Could not verify validate\_live.
The verifier did not have enough to check validate\_live, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify scoped\_root\_cdp.
The verifier did not have enough to check scoped\_root\_cdp, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify scoped\_events.
The verifier did not have enough to check scoped\_events, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify reply.
The verifier did not have enough to check reply, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify execute\_registered\_tool.
The verifier did not have enough to check execute\_registered\_tool, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify tool\_ctx.
The verifier did not have enough to check tool\_ctx, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify bound\_raw\_policy\_and\_explicit\_window\_group\_conflicts\_fail\_before\_dispatch.
The verifier did not have enough to check bound\_raw\_policy\_and\_explicit\_window\_group\_conflicts\_fail\_before\_dispatch, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: anyhow::Result<()>
Could not verify: Could not verify capable\_native\_exposes\_run\_profile\_exports.
The verifier did not have enough to check capable\_native\_exposes\_run\_profile\_exports, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: anyhow::Result<()>
Could not verify: Could not verify discovery\_omits\_profile\_surfaces\_for\_older\_natives.
The verifier did not have enough to check discovery\_omits\_profile\_surfaces\_for\_older\_natives, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify reply.
The verifier did not have enough to check reply, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported: method receiver/self type not synthesizable from the simple-type literal pools — abstaining rather than guessing a receiver
Could not verify: Could not verify target\_output\_schema\_preserves\_full\_readback\_through\_content\_gate.
The verifier did not have enough to check target\_output\_schema\_preserves\_full\_readback\_through\_content\_gate, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: anyhow::Result<()>
Could not verify: Could not verify handler.
The verifier did not have enough to check handler, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: BoxFuture<'a,ToolExecResult<Option<ToolResult>>>
· 1 more finding(s) on lines outside this diff (see the check run).
Placement is captured once, so pages opened through one call (several browser.open in a run, or Promise.all) each opened a new window when Default had none. The admitted view now remembers the window it opened, single-flight across clones, and reuses it while it still validates.
Summary
Follow-up to #2918. On a native with profile targeting, a task with no saved
browser_targetnow runs in the browser's original Chromium profile: catalog directory"Default", never its display name. It no longer lands in whatever window happened to be focused last.An explicit target still wins, and clearing it returns later calls to Default. There is no UI yet; the choice lives in one selector so a later setting can replace it.
One decision for every path. Every host already captures through
BrowserView::capture: clawservice.rs, the standaloneBrowserMcpServiceand theBrowserfacade. So granular tools, run direct primitives,tool:*, helpers and raw CDP all inherit the same Default scope.browser_target getwith nothing stored returns:{ "bound": false, "default": { "directory": "Default", "status": { "profile": …, "windows": […], "placementWindowId": …, "error": … } } }The schema gains only that optional
defaultobject. The profile guidance and therun/tabs/windows/browser_targetnotes now describe the Default rule.Evidence
Before (
1638e4a9d): a capable native with no target ran unbound:tabs new/browser.openused the last-active window across all profiles, andwindows createused that window's profile. That is the source auditbrowser-opening-profile-audit-20261014.After (mocked-CDP behavior tests through the real interfaces):
no_target_uses_the_original_default_profile_not_focus_or_display_name(renamed Default; another profile display-named "Default"; the sales window focused): listing,windows list,new_page,create_window, rawcreateTabandHistoryall use Default's window/context, and another profile's page →page_out_of_scope.default_without_a_window_opens_an_inactive_normal_window_there: listing opens nothing, thencreateWindow {context, normal}andcreateTab {url, windowId}. RawcreateTab {pinned}keeps its arguments. An explicit target with no window still givesno_profile_window.unloaded_default_is_loaded_existing_only_before_admissionunusable_default_fails_admission_without_using_another_profile(missing / load failed: zero creations, management works)explicit_target_wins_over_the_defaultdefault_handles_are_fresh_after_a_native_restart(a stale explicit handle still givesprofile_gone)admitted_default_view_fails_closed_after_a_reconnectnatives_without_profile_targeting_keep_unbound_routing_with_no_target(zerogetProfiles/loadProfile/ context params)management_capture_never_loads_the_defaultfacade_never_falls_back_to_global_routing_without_a_defaultfailed_default_view_exposes_no_global_eventstools_and_run_share_the_default_profile_without_a_selection(tabs new,browser.open,pages.newPage,pages.list,windows,history, rawHistory)default_without_a_window_gets_one_from_tabs_and_runexplicit_selection_overrides_default_and_clear_restores_it(strict pin stays strict)an_admitted_default_view_ignores_a_later_selectionmissing_default_refuses_work_but_selection_recoversprofile_management_never_loads_an_unloaded_defaulttarget_output_schema_…readback andbound_raw_policy_…(clear returns to the Default scope, not raw passthrough)unselected_run_uses_the_default_profile_and_keeps_it_after_rebind(run creation and detached grouping land in Default's window after a laterset; the binding store holds only the explicitset)browser_target_wire(getreports Default beforesetand afterclear)MockCdpserves no profile catalog, windows or identity sidecars, so those session/tab tests now say it is a pre-0.51.1.3 native.Verification:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --locked -- -D warningscargo test --workspace --locked: 1103 passed, 0 failedgit diff --checkNot claimed: no live native acceptance was run; the evidence is fixtures plus CI. On natives without profile targeting (< 0.51.1.3 or unknown), Default routing is not enforced; they keep the feat(claw): gate native profile features on the BrowserOS version #2918 unbound path.
Independent review: one Codex round on
85e4cf6a3raised 4 Important findings; all were accepted and fixed in66344d475.Browserfacade's unbound shortcut. The same pattern was found and fixed inscoped_events.getActiveTab. It is now window first, thencreateTab.createTabdropped its args on that path. Only the window is supplied now.capture_for_management, which never loads.Greptile P2 (this PR): each page opened its own window when Default had none. The admitted view now opens at most one, single-flight (
2011462f8). Covered byone_admitted_default_view_opens_at_most_one_windowandone_run_without_a_default_window_opens_one_window_for_all_pages.Merge Danger
Door: two-way. This is a code-only routing change. Nothing new is persisted, and saved explicit bindings are untouched.
Blast Radius:
browser_target set.clearreturns to Default. OAuth /window.openpopups (non-normal windows) were already outside explicit targets, and are now also outside the default scope.Defaultdirectory, or one whose Default fails to load, refuses ordinary calls withdefault_profile_unavailableuntil a profile is selected.🤖 Generated with Claude Code