Repository navigation
feat(claw): gate native profile features on the BrowserOS version - #2918
Conversation
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.
…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.
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 122 changed function(s) (122 not verified).
Formal verification. PR-changed functions: 0/122 verified (0 proven, 0 may-equivalent, 0 distinguished) · 122 not verified (25 unsupported, 97 over cap).
Not verified on this run: assert\_deferred\_grouping\_preserves\_explicit\_group (unsupported), assert\_run\_deferred\_grouping\_preserves\_explicit\_group (unsupported), deferred\_grouping\_observes\_new\_native\_group\_during\_lookup (unsupported), production\_pipeline\_keeps\_audit\_in\_the\_observer\_lane (unsupported), queued\_group\_call (unsupported), run\_creation\_and\_hook\_keep\_admitted\_view\_after\_rebind (unsupported), stale\_binding\_blocks\_execution\_while\_management\_recovers (unsupported), a\_failed\_group\_listing\_still\_adopts\_the\_named\_group (unsupported), a\_page\_another\_client\_holds\_is\_left\_alone (unsupported), a\_session\_that\_already\_has\_a\_group\_gets\_no\_nudge (unsupported), and 15 more; 97 changed function(s) beyond the cap and 0 skipped when the time budget ran out.
Graphify review — findings
Adds a scope-notice dispatch effect that appends a label to the outer MCP result when a saved profile target couldn't be applied because the connected native browser is too old. In that case the call runs with the unbound, browser-wide routing those natives always had, so work isn't blocked, the saved binding stays intact, and the notice shows up once. Stale clients that still hold the hidden profile tools get refused with native_feature_unavailable: profile targeting without any profile CDP traffic. Primitives inside a run script don't pass through dispatch effects, so their text is left untouched. Tests now build sessions through a shared current_session helper that assumes a current native, and one new test covers the older-native downgrade path.
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 — 1420 functions depend on the 1318 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
assert_deferred_grouping_preserves_explicit_group()— 4 callers, 4 callees - worse:
assert_change_during_group_lookup()— 4 callers, 3 callees
Verification — 1420 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: 1413 function(s) in the blast radius were not formally verified this run
Docs that may be stale (advisory)
.claude/skills/write-internal-docs/SKILL.md§ Step 3: Draft the Markdown (lines 83-92): references changed symbolsspec.claude/skills/writing-plans/plan-document-reviewer-prompt.md§ Plan Document Reviewer Prompt Template (lines 1-49): references changed symbolsspecdocs/browseros/features/bring-your-own-llm.mdx§ Bring Your Own Kimi API Key (lines 79-100): references changed symbolskeydocs/browseros/features/bring-your-own-llm.mdx§ Cloud Providers (lines 101-261): references changed symbolskeydocs/browseros/features/use-with-claude-code.mdx§ Example Prompts (lines 188-223): references changed symbolscurrentdocs/neo/mcp/claude-desktop.mdx§ Verify (lines 27-36): references changed symbolsnewpackages/browseros-agent/.agents/skills/ai-sdk/references/common-errors.md§ Manual JSON parsing → generateText with output (lines 111-139): references changed symbolsparsepackages/browseros-agent/.agents/skills/ai-sdk/references/common-errors.md§ Removed managed input state in useChat (lines 192-242): references changed symbolsnewpackages/browseros-agent/.agents/skills/ai-sdk/references/common-errors.md§ tool-invocation → tool-{toolName} (typed tool parts) (lines 243-306): references changed symbolskeypackages/browseros-agent/.agents/skills/ai-sdk/references/common-errors.md§ useChat state-dependent property access (lines 307-338): references changed symbolsavailable
…and 10 more.
Formal verification
Could not verify: Could not verify assert\_deferred\_grouping\_preserves\_explicit\_group.
The verifier did not have enough to check assert\_deferred\_grouping\_preserves\_explicit\_group, 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 assert\_run\_deferred\_grouping\_preserves\_explicit\_group.
The verifier did not have enough to check assert\_run\_deferred\_grouping\_preserves\_explicit\_group, 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 deferred\_grouping\_observes\_new\_native\_group\_during\_lookup.
The verifier did not have enough to check deferred\_grouping\_observes\_new\_native\_group\_during\_lookup, 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 production\_pipeline\_keeps\_audit\_in\_the\_observer\_lane.
The verifier did not have enough to check production\_pipeline\_keeps\_audit\_in\_the\_observer\_lane, 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 queued\_group\_call.
The verifier did not have enough to check queued\_group\_call, 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<(ToolCall,Arc<crate::api::mcp::test_support::ProfileConnection>,)>
Could not verify: Could not verify run\_creation\_and\_hook\_keep\_admitted\_view\_after\_rebind.
The verifier did not have enough to check run\_creation\_and\_hook\_keep\_admitted\_view\_after\_rebind, 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 stale\_binding\_blocks\_execution\_while\_management\_recovers.
The verifier did not have enough to check stale\_binding\_blocks\_execution\_while\_management\_recovers, 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 a\_failed\_group\_listing\_still\_adopts\_the\_named\_group.
The verifier did not have enough to check a\_failed\_group\_listing\_still\_adopts\_the\_named\_group, 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 a\_page\_another\_client\_holds\_is\_left\_alone.
The verifier did not have enough to check a\_page\_another\_client\_holds\_is\_left\_alone, 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 a\_session\_that\_already\_has\_a\_group\_gets\_no\_nudge.
The verifier did not have enough to check a\_session\_that\_already\_has\_a\_group\_gets\_no\_nudge, 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 assert\_change\_during\_group\_lookup.
The verifier did not have enough to check assert\_change\_during\_group\_lookup, 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 assert\_creation\_survives\_response\_handoff.
The verifier did not have enough to check assert\_creation\_survives\_response\_handoff, 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 borrowed\_group\_cleanup\_leaves\_its\_tabs\_and\_group\_open.
The verifier did not have enough to check borrowed\_group\_cleanup\_leaves\_its\_tabs\_and\_group\_open, 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 borrowing\_a\_group\_keeps\_an\_earlier\_sessions\_actual\_ownership.
The verifier did not have enough to check borrowing\_a\_group\_keeps\_an\_earlier\_sessions\_actual\_ownership, 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 borrowing\_a\_group\_keeps\_claims\_even\_when\_the\_owner\_is\_retired.
The verifier did not have enough to check borrowing\_a\_group\_keeps\_claims\_even\_when\_the\_owner\_is\_retired, 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 concurrent\_first\_pages\_share\_one\_created\_group.
The verifier did not have enough to check concurrent\_first\_pages\_share\_one\_created\_group, 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 connected\_call.
The verifier did not have enough to check connected\_call, 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<(ToolCall,Arc<BrowserSession>)>
Could not verify: Could not verify delayed\_bound\_grouping\_preserves\_each\_actual\_window\_and\_revision.
The verifier did not have enough to check delayed\_bound\_grouping\_preserves\_each\_actual\_window\_and\_revision, 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 delayed\_grouping\_refuses\_replacement\_of\_created\_page\_incarnation.
The verifier did not have enough to check delayed\_grouping\_refuses\_replacement\_of\_created\_page\_incarnation, 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 delayed\_unbound\_grouping\_keeps\_creation\_epoch\_before\_callback.
The verifier did not have enough to check delayed\_unbound\_grouping\_keeps\_creation\_epoch\_before\_callback, 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 delayed\_unbound\_grouping\_recovers\_its\_original\_window\_after\_binding.
The verifier did not have enough to check delayed\_unbound\_grouping\_recovers\_its\_original\_window\_after\_binding, 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 dragged\_group\_is\_detached\_from\_placement\_but\_retained\_for\_cleanup.
The verifier did not have enough to check dragged\_group\_is\_detached\_from\_placement\_but\_retained\_for\_cleanup, 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 each\_group\_dispatch\_uses\_the\_shared\_timeout.
The verifier did not have enough to check each\_group\_dispatch\_uses\_the\_shared\_timeout, 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 effect\_returns\_before\_group\_creation\_finishes.
The verifier did not have enough to check effect\_returns\_before\_group\_creation\_finishes, 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 explicit\_borrowed\_group\_in\_same\_capture\_preserves\_owned\_group\_cleanup.
The verifier did not have enough to check explicit\_borrowed\_group\_in\_same\_capture\_preserves\_owned\_group\_cleanup, 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<()>
· 2 more finding(s) on lines outside this diff (see the check run).
✅ Tests passed: 1236/1236Ran 3 of 16 suites (13 not affected by this change).
|
[High impact] The PR appears safe to merge; no actionable issues were found. SummaryGates profile features on the native BrowserOS version, starting at 0.51.1.3.
Intentional choices acknowledged by shadowfax92:
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Launch config or explicit version override"] --> B["NativeCapabilities"]
B --> C{"ProfileTargeting supported?"}
C -->|Yes| D["Advertise profile tools and SDK exports"]
C -->|Yes| E["Capture saved target with live checks"]
C -->|No| F["Hide profile features and refuse gated calls"]
C -->|No| G["Keep saved target; route browser-wide"]
G --> H["Append one notice to outer result"]
Reviews (1) · Last reviewed commit: "fix(claw): gate typed tab creation with ..." · Reviewed by Greptile |
#2919) * feat(claw): use the original Default profile when a task has no target 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. * fix(claw): harden default-profile capture after review - 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. * fix(core): open at most one Default window per admitted view 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
claw-server ships by OTA ahead of the native browser, so a new server can drive a BrowserOS/Neo build that predates the #2908 profile bundle (first native release: 0.51.1.3). This adds one native capability gate, keyed on the browser's own version from the launch config.
instanceis unknown. Unknown hides and refuses table features, never blocks startup, and leaves ordinary browsing untouched.browserContextIdlabels.get_tool/call_toolstay catalog-wide (stable rmcp schemas), and cached callers get the refusal. The JS flags only shape discovery; Rust is authoritative.browserContextId(e.g.Target.createTarget,Storage.*) and calls without the gated option pass through unchanged. The existingnative_missingbackstop is unchanged.10000.-prefixedCFBundleVersion(the stamp the release pipeline writes before signing; the signature itself is not verified); otherwise the identity stays unknown.Assumption (documented, not new infrastructure): the launch config describes the browser that started this server under the normal managed lifecycle; a different browser comes with a relaunched server.
Evidence
Before (8fc4658):
Browser.getProfilesfailed every ordinary call withtarget_scope_unsupported_native(oldold_native_is_explicitly_unsupported_while_unbound_raw_is_unchanged).tools/list, instructions and the run SDK always advertisedbrowser_profiles/browser_target.instance.After:
profile_targeting_starts_at_native_0_51_1_3versions_compare_numerically_not_lexically(.10 vs .9)missing_blank_or_malformed_identity_is_unknownraw_guard_refuses_listed_methods_and_parameter_pairs_only(createTab/createWindow refused; stock OTR allowed)native_without_profile_targeting_runs_a_saved_binding_unbound(old + unknown: no admission error, zerogetProfiles/loadProfile/ gated sends, createTab without context still sent)capable_native_retries_a_retained_binding_with_live_checks(live catalog re-read each capture; restarted handles →profile_gone)typed_tab_creation_with_a_context_needs_profile_targetingolder_native_refuses_profile_tools_and_run_primitives_before_sending(direct calls and raw__browserosCall('tool:browser_profiles')refused;typeof browser.profiles === 'undefined')capable_native_exposes_run_profile_exportsdiscovery_omits_profile_surfaces_for_older_nativeshidden_tools_keep_stable_schemas_for_cached_clientssaved_binding_on_an_older_native_runs_unbound_with_one_outer_notereads_the_browser_written_native_version,missing_blank_or_wrong_typed_instance_is_unknown_and_still_starts,developer_assertion_overrides_and_is_markedolder_native_runs_saved_binding_unbound_and_refuses_hidden_tools(full dispatch pipeline)older_native_lists_and_teaches_no_profile_surfaceprofile_guidance_is_offered_only_to_capable_natives10000.0.51.1.3→0.51.1.3;8309.26/ missing → blank).browser_profilescall refused.Verification:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --locked -- -D warningscargo test --workspace --locked: 1083 passedgo vet/go testintools/devandtools/dogfoodbun run lintgit diff --checkNot claimed: no live acceptance against a real older or 0.51.1.3 native was run locally. The mocked-CDP fixtures model those browsers, and the claw-mcp contract exercises the unknown-identity path on the public AppImage in CI.
Independent review: one Codex round on
aff99de36raised 2 Important findings.BrowserView::new_pagewith abrowser_context_idnow requires profile targeting. Covered bytyped_tab_creation_with_a_context_needs_profile_targeting(red → green).Merge Danger
Door: two-way. Behavior is code-only and reverts cleanly. Persisted bindings are never rewritten.
Blast Radius:
instance.browseros_version; those can setCLAW_NATIVE_BROWSEROS_VERSION.browserContextIdonHistory.*andBrowser.createTab/createWindowis refused there.🤖 Generated with Claude Code