Expose the 7 Klavis Strata MCP tools as CLI subcommands under
`browseros-cli strata`, so CLI users (claude-code, gemini-cli) can
discover and execute actions on 40+ external services.
Commands: check, discover, actions, details, exec, search, auth.
Includes discovery flow guidance in help text, integration tests,
and an "Integrations:" group in the root help output.
Agents connecting over MCP URL/CLI (like claude-code) had no way to know
which Klavis connectors were available or authenticated, causing them to
fall back to browser automation. This adds a connector_mcp_servers tool
that checks connection status and returns an auth URL when needed.
* fix(openclaw): compose file path after service dir move, loopback auth fallback
- Fix COMPOSE_RESOURCE path: services moved to api/services/openclaw/
so the relative path needs one more parent directory traversal
- Fix requireTrustedAppOrigin middleware: Chrome extensions cannot set
the Origin header (forbidden header name). When Origin is absent,
fall back to checking the Host header is a loopback address. The
server only binds to loopback so only local processes can reach it.
Requests with an explicit non-trusted Origin are still rejected.
* fix: request header check
* chore: remove setup openclaw button
---------
Co-authored-by: Dani Akash <DaniAkash@users.noreply.github.com>
Move openclaw/ and terminal/ service modules from src/services/ into
src/api/services/ so all server-side services live in one directory
alongside chat-service, klavis, mcp, and sdk. Update relative imports
in moved files and all callers.
- Add tool approval system with per-category approval configuration
- Build unified Governance dashboard (renamed from Admin) with pending
approvals view and execution audit log
- Move execution history tracking into the app shell
- Extract buildChatRequestBody helper and add newtab system prompt
- Add approval config change detection for mid-conversation rebuilds
* feat: add ACL rules for per-site element-level agent restrictions
Implement Access Control List (ACL) rules that let users block the agent
from interacting with specific elements on specific websites. Rules are
defined in a new Settings > ACL Rules page and enforced server-side in
executeTool() before any input tool handler runs.
- Shared ACL types and site pattern matching (packages/shared)
- Extension storage, settings UI with rule cards and add dialog
- Server-side guard in executeTool() checking tool+page+element
- Browser class extensions for element property resolution via CDP
- Visual overlay injection (red "BLOCKED" mask) via Runtime.evaluate
- Rules transported in chat request body alongside declinedApps
* fix: address review comments for ACL rules
- Add selector-to-property matching in matchesElement (tag, id, class)
- Remove scroll from guarded tools set (read-like action)
* fix: ACL site pattern matching fails on multi-segment URL paths
The glob-to-regex conversion used [^/]* for wildcard (*) which only
matches a single path segment. "*.amazon.com/*" failed to match
"www.amazon.com/cart/smart-wagon" because the trailing * couldn't
cross the slash between "cart" and "smart-wagon".
Fix: Split URL matching into hostname vs path parts. Path wildcards
now use .* to match across slashes. Also add simple domain matching
so users can just type "amazon.com" instead of "*.amazon.com/*".
* fix: wire up ACL overlay injection after take_snapshot
applyAclOverlays was defined but never called. Now triggers after
take_snapshot completes on pages matching ACL rules, so the agent
sees red "BLOCKED" overlays on restricted elements.
* refactor: rework 0326-acl_rules based on feedback
* feat(openclaw): add foundation — paths constant, browseros-dir helper, static compose file
Add OPENCLAW_DIR_NAME to shared paths constant, getOpenClawDir() to
browseros-dir.ts, and a static docker-compose.yml resource file that
uses native .env variable substitution instead of YAML template strings.
* feat(openclaw): add PodmanRuntime container engine abstraction
Manages Podman CLI interactions: machine lifecycle (init/start/stop),
availability checks, command execution with streaming output, and
running container enumeration. Linux skips machine ops since Podman
runs natively.
* feat(openclaw): add config builder and container runtime
openclaw-config.ts: pure functions to build openclaw.json and .env files
from BrowserOS settings. Maps provider keys, sets permissive defaults
(full exec, cron, web search, MCP bridge to BrowserOS).
container-runtime.ts: compose-level abstraction over PodmanRuntime for
the browseros-openclaw project. Handles up/down/restart/pull, health
checks, .env file writes, and safe machine shutdown.
* feat(openclaw): add OpenClawService orchestrator
Main service managing the single OpenClaw container. Handles full
lifecycle (setup/start/stop/restart/shutdown), agent CRUD with config
rewrites and gateway restarts, chat proxy to /v1/chat/completions,
provider key updates, auto-start on BrowserOS boot, and status reporting.
* feat(openclaw): add API routes and server wiring
Add /api/claw/* routes for container lifecycle (setup/start/stop/restart),
agent CRUD (list/create/delete), chat proxy with SSE streaming, provider
key management, and log retrieval. Register routes in server.ts, add
OpenClaw auto-start on BrowserOS boot and graceful shutdown in main.ts.
* fix(openclaw): resolve type errors in service and podman runtime
Fix TIMEOUTS.TOOL_EXECUTION → TIMEOUTS.TOOL_CALL to match shared
constants. Fix ReadableStream undefined/null type mismatch in
PodmanRuntime.runCommand stream draining.
* feat(openclaw): add agents page UI with chat, create, and lifecycle controls
Add /agents route with AgentsPage showing OpenClaw status, agent list,
create dialog, and per-agent chat. Includes useOpenClaw hook for
server communication, AgentChat component with SSE streaming, and
sidebar navigation entry.
* feat(openclaw): add provider selector to setup flow
Add LLM provider selector using useLlmProviders hook. Filters out
OAuth-only providers, pre-selects the user's default, and passes
providerType/apiKey/modelId to the setup endpoint so OpenClaw gets
a working LLM configuration on first setup.
* feat(openclaw): per-agent provider selection
Each agent can now have its own LLM provider. The Create Agent dialog
includes a provider selector that passes providerType/apiKey/modelId
to the backend. The service writes per-agent model config to
openclaw.json and merges the API key into the container's .env file.
* fix(openclaw): write gateway auth token to openclaw.json
The gateway was returning 401 because auth.mode was set to "token"
without providing the actual token value. Now the token is written
to gateway.auth.token in openclaw.json so the gateway and our chat
proxy agree on the same token.
* feat(openclaw): add GatewayClient WebSocket RPC client
Persistent WS client for the OpenClaw Gateway protocol. Handles the
challenge → connect → hello-ok handshake (as openclaw-control-ui with
operator.admin scope), JSON-RPC with pending map + timeouts, and
auto-reconnect. Exposes typed methods for agents.list, agents.create,
agents.delete, and health.
* refactor(openclaw): simplify config to bootstrap-only, add /readyz health
Config no longer contains agents.list — agent CRUD is handled via WS RPC.
buildOpenClawConfig → buildBootstrapConfig, removed makeAgentEntry and
AgentEntry (agents managed by OpenClaw runtime). Added isReady() and
waitForReady() using /readyz for gateway readiness checks.
* refactor(openclaw): agent CRUD via WS RPC, per-agent chat targeting
Replace JSON mutation + restart with GatewayClient WS RPC calls for
agents.create, agents.delete, agents.list. Chat proxy now uses
model: "openclaw/<agentId>" for per-agent targeting. Setup writes
bootstrap config once then creates "main" agent via WS after gateway
starts. Container restarts only when a new provider env var is added.
* fix(openclaw): use agentId field in setup response mapping
Fix type error: GatewayAgentEntry uses agentId not id.
* fix(openclaw): log service progress through server logger
* feat(openclaw): WS streaming, device auth, MCP port fix (#687)
* feat(openclaw): WS streaming, device auth, MCP port fix
- Fix GatewayClient WS handshake: add Ed25519 device identity signing,
Origin header, mode: cli (mode: ui requires device identity always)
- Add auto device pairing flow: generate client identity, attempt WS
connect (triggers pending), approve via openclaw CLI, reconnect
- Replace HTTP /v1/chat/completions proxy with WS-based streaming that
surfaces tool calls, thinking blocks, and text deltas
- Add chatStream() to GatewayClient returning ReadableStream of typed
OpenClawStreamEvent (text-delta, thinking, tool-start/end, lifecycle)
- Update chat route to stream WS events as SSE to the extension
- Pass actual server port to OpenClaw config (fixes MCP bridge in dev)
- Rewrite AgentChat.tsx with turn-based model using Message/MessageContent
components matching sidepanel pattern, with tool batching logic that
groups consecutive tools and breaks on text/thinking (same as sidepanel)
- Add execInContainer() to ContainerRuntime for CLI commands
- Fix gateway response field mapping (id→agentId, agents.list/create)
- Skip creating main agent if gateway auto-creates it
* fix(openclaw): retry WS connect on signature expired (Podman clock skew)
Podman VM clock drifts when Mac sleeps, causing Ed25519 signature
validation to fail with "device signature expired" on auto-start.
Add connectGatewayWithRetry() that restarts the container (resyncs
clock) and re-approves the device if needed.
* fix(openclaw): address PR review — stream cleanup, error handling
- Fix silent catch in setup(): only swallow "pairing required" and
"signature expired" errors, re-throw everything else
- Guard JSON.parse in approvePendingDevice(): check exit code and
wrap parse in try/catch with descriptive error messages
- Add try/finally in chat SSE route: reader.cancel() on disconnect
- Add cancel callback to chatStream ReadableStream: restores
ws.onmessage when stream is cancelled (prevents handler leak)
---------
Co-authored-by: shivammittal274 <56757235+shivammittal274@users.noreply.github.com>
* fix: enable agent interaction with elements inside iframes
Fetch accessibility trees from all frames via Page.getFrameTree() +
per-frame Accessibility.getFullAXTree(frameId), so iframe elements
appear in snapshots with valid backendNodeIds. Pages without iframes
take the original single-call path with zero overhead.
Update snapshot tree builders to walk multiple RootWebArea roots from
merged multi-frame trees. Extract same-origin iframe content in the
markdown walker; show [iframe: url] placeholder for cross-origin.
* fix: namespace AX nodeIds by frameId to prevent cross-frame collisions
CDP AXNodeId values are frame-scoped — each frame's accessibility tree
starts its own counter from 1. Prefix nodeId and childIds with frameId
before merging so the nodeMap in snapshot builders never overwrites
nodes from a different frame.
* docs: add uBlock Origin install info to getting started and ad-blocking pages
Chrome dropped support for the full uBlock Origin extension — highlight
that BrowserOS brings it back and make it easy to install from both the
getting started guide and the dedicated ad-blocking page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: revert Kimi partnership UI, restore daily limit survey
Remove Kimi/Moonshot AI partnership branding from the rate limit
banner, provider card, provider templates, and LLM hub. Restore
the original survey CTA on daily limit errors. Moonshot AI remains
as a regular provider template without the "Recommended" badge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review comments
- Guard survey CTA with !isCreditsExhausted to avoid showing it for
credits-exhausted users who already see "View Usage & Billing"
- Remove dead kimi-launch feature flag files (kimi-launch.ts,
useKimiLaunch.ts)
- Remove unused KIMI_RATE_LIMIT analytics events
- Remove VITE_PUBLIC_KIMI_LAUNCH from env schema and .env.example
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The merged PR (#661) injected custom entries into filteredModels, but
cmdk auto-scrolls to its first selected CommandItem, pushing the custom
entry out of view. Fix by using forceMount on a separate CommandGroup
and resetting scroll to top on every keystroke via requestAnimationFrame.
* feat: show custom model ID as first option in model selector
When typing in the model dropdown, the user's exact input now appears as the
first selectable row, followed by fuzzy search suggestions. This makes entering
custom model IDs intuitive — previously the option was hidden behind a
zero-results-only Enter shortcut that fuzzy search almost always prevented.
* fix: correct is_custom_model flag and prevent duplicate analytics events
- Use modelInfoList check instead of hardcoding is_custom_model: true in
the Enter key handler
- Add stopPropagation to prevent cmdk's root keydown handler from also
firing onSelect, which caused duplicate MODEL_SELECTED_EVENT emissions
* fix: install linux sysroot in configure, not via gclient hook
`gn gen` was failing on the arm64 leg with `Missing sysroot
(//build/linux/debian_bullseye_arm64-sysroot)`. The previous design
relied on `git_setup` writing `target_cpus` to `.gclient` so that
`gclient sync`'s DEPS hook would download the cross-arch sysroot. That
chain breaks for any chromium_src that was synced before cross-arch
support landed (the hook is gated on .gclient state at sync time) and
for partial pipeline runs that skip git_setup entirely. Nothing in
configure declared or verified its sysroot precondition.
Make configure self-healing: on Linux, invoke
`build/linux/sysroot_scripts/install-sysroot.py --arch=<target>`
directly before `gn gen`. install-sysroot.py is idempotent (stamp file
+ SHA check), fast when already installed, and decoupled from .gclient
— it's exactly what the failing assertion's error message recommends.
The script accepts our arch names directly: `x64` translates to `amd64`
internally via ARCH_TRANSLATIONS, and `arm64` is a valid pass-through.
Also temporarily pin release.linux.yaml to x64 only while we validate
the sysroot bootstrap end-to-end. Flip back to `[x64, arm64]` once
arm64 is green.
* chore: pin release.linux.yaml to arm64-only for sysroot bootstrap test
x64 already builds cleanly — the failing leg is arm64 cross-compile from
an x64 host. Pin the config to arm64 to exercise the new
install-sysroot.py path in configure without burning time on x64.
Flip back to [x64, arm64] once arm64 is green.
* feat(server): cache klavis createStrata to unblock /chat hot path
Conversation creation in /chat was blocking on a Worker-proxied
klavisClient.createStrata round-trip every time the user had any
managed Klavis app connected. The 5s KLAVIS_TIMEOUT_MS in the
ai-worker proxy existed specifically to bound this latency, but
the same cap also caused user-visible 504s on /klavis/servers/remove
since Strata DELETE operations routinely take >5s. Without caching
we couldn't raise the timeout without regressing chat creation.
This adds an in-process cache for Strata createStrata responses,
keyed by (browserosId, hashed sorted-server-set) and gated by a 1h
TTL. The cache stores only immutable JSON metadata (strataServerUrl,
strataId, addedServers); per-session MCP clients continue to be
opened and disposed by AiSdkAgent exactly as before, which keeps
the cache concurrency-safe by construction.
Cache invalidation has two layers: (a) the cache key embeds the
server set, so adding/removing apps naturally produces a different
key; (b) POST /klavis/servers/add and DELETE /klavis/servers/remove
explicitly call invalidate(browserosId) after their underlying
Klavis API call succeeds, as defense-in-depth.
Other changes:
- Consolidates klavis-related services into a new
apps/server/src/api/services/klavis/ directory; moves
register-klavis-mcp.ts -> strata-proxy.ts and adds strata-cache.ts
there. lib/clients/klavis/ stays unchanged.
- Refactors KlavisClient.removeServer into a low-level
deleteServersFromStrata(strataId, servers) primitive. The
cache-lookup + delete + invalidate orchestration moves up into
routes/klavis.ts where it belongs, eliminating the lib->api
layering inversion the original removeServer would have introduced.
- Uses Bun.hash (xxhash64) for fixed-width 16-hex-char keys, with
serverKey verified on read to make collision risk strictly zero.
- Dedupes concurrent fetches via in-flight Promise sharing, with
identity-checks before delete to avoid races between invalidate()
and a racing replacement insert.
Follow-up (separate PR): bump KLAVIS_TIMEOUT_MS to 30000 in
ai-worker/wrangler.toml so /klavis/servers/remove stops 504-ing.
* fix: address greptile review comments for klavis strata cache
- Drop dead `invalidated` field on InflightEntry. It was added to
support a "discard post-resolution if invalidated" check that I
later replaced with identity-checked deletes during self-review,
but I forgot to remove the field and the misleading comment
referencing it. Simplify Map<string, InflightEntry> to plain
Map<string, Promise<CacheEntry>>.
- Lower cache miss log from info to debug. Misses fire on every new
conversation; matching the existing debug-level for hits.
- Stop routing the /klavis/servers/remove handler through
klavisStrataCache.getOrFetch. The chat hot path keys its cache by
the user's full enabled-server set (e.g. hash('Gmail,Linear')),
so a single-server lookup here (hash('Gmail')) is guaranteed to
miss, write a spurious entry, and then have it immediately
cleared by invalidate() on the next line. Call createStrata
directly to recover the strataId, mirroring the original
removeServer flow.
`release.linux.yaml` now declares `architecture: [x64, arm64]` and the
runner loops the entire pipeline once per architecture. depot_tools
fetches both Linux sysroots automatically — `git_setup` idempotently
ensures `target_cpus = ['x64', 'arm64']` is in `.gclient` before
`gclient sync`, so cross-compiling arm64 from an x64 host just works.
The resolver returns `List[Context]` (single-element for the common
single-arch case), and `build/cli/build.py` loops `execute_pipeline` over
the per-arch contexts. Modules stay 100% arch-agnostic — no new
orchestration module, no new YAML schema beyond the list form.
Also fix a cross-compile bug in `build/modules/package/linux.py`: the
appimagetool binary must match the BUILD machine's arch (it executes
locally), not the target arch. Split into a host-keyed
`LINUX_HOST_APPIMAGETOOL` lookup vs the existing target-keyed
`LINUX_ARCHITECTURE_CONFIG`. Target arch is still passed to appimagetool
via the `ARCH` env var.
- build/common/resolver.py: scalar OR list `architecture` -> List[Context]
- build/cli/build.py: loop pipeline per arch, log multi-arch headers
- build/config/release.linux.yaml: `architecture: [x64, arm64]`
- build/modules/setup/git.py: idempotent `target_cpus` edit on Linux
- build/modules/package/linux.py: host vs target appimagetool split
- build/modules/package/linux_test.py: cover the host/target split
The --compile-only and --ci flags served overlapping purposes for CI
builds. Remove --compile-only entirely since --ci already handles the
CI use case (skip R2, skip prod env validation, local zip packaging)
and --no-upload covers the upload-skipping use case for full builds.
The server release CI workflow fails on ubuntu-latest because
patch-windows-exe.ts requires Wine to run rcedit. Thread the existing
--ci flag through compileServerBinaries so Windows PE metadata patching
is skipped in CI mode with a warning log.
* feat: add server release workflow
* fix: address PR review comments for 0331-add_server_release_workflow
* refactor: rework 0331-add_server_release_workflow based on feedback
* refactor: rework 0331-add_server_release_workflow based on feedback
* feat(cli): skip self-update prompts for package manager installs
Checks BROWSEROS_INSTALL_METHOD env var (npm, brew) and skips automatic
update checks. Users should use their package manager's update mechanism.
FormatNotice now shows the appropriate upgrade command based on install method.
* feat(cli): add npm bin wrapper for browseros-cli
* feat(cli): add npm postinstall script to download platform binary
Downloads the correct platform binary from GitHub releases during npm
install, verifies SHA256 checksums, and extracts to .binary directory.
* feat(cli): add npm package metadata and README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move npm package files to correct monorepo path
The bin wrapper and postinstall were created at apps/cli/npm/ instead of
packages/browseros-agent/apps/cli/npm/. Moves them to the correct location.
* style: use node: protocol for builtin module imports
* feat(cli): add Makefile npm targets and release workflow npm publish step
Adds npm-version and npm-publish Makefile targets for version sync.
Adds Node.js setup and npm publish step to the release workflow.
Adds npm/npx install instructions to release notes template.
* fix(cli): fail on missing checksum entry and limit redirect depth
- Abort if checksums.txt downloaded but archive entry is missing
- Warn if checksums.txt itself failed to download
- Cap redirect depth at 5 to prevent stack overflow on circular redirects
* fix(cli): match install.sh checksum behavior — warn instead of abort
The existing shell installer (install.sh) warns and continues when the
checksum entry is missing from checksums.txt. Match that behavior in the
npm postinstall to avoid unnecessary install failures. Both files come
from the same GitHub release, so the checksum is a corruption check,
not a strong security boundary.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The model picker in NewProviderDialog rendered inline, causing dialog
resizing and lacked keyboard navigation. Replace it with a Popover +
Command (shadcn Combobox) pattern and add fuse.js for fuzzy search.
- Replace custom ModelPickerList with Popover + Command dropdown
- Add fuse.js for fuzzy model search (replaces string.includes)
- Add MODEL_SELECTED_EVENT and AI_PROVIDER_UPDATED_EVENT analytics
- Enrich PROVIDER_SELECTED_EVENT with model_id in chat sessions
* feat: add browseros-cli self-updater
* fix: address review comments for 0327-cli_self_updater
* fix: address PR review comments for 0327-cli_self_updater
* fix: replace goreleaser with Makefile-based release build
Remove .goreleaser.yml (required Pro license for monorepo field) and
consolidate cross-compilation into `make release`. CI now uses the same
Makefile target, fixing a bug where POSTHOG_API_KEY was missing from
release ldflags.
* fix: address critical self-updater bugs from code review
- Fix SHA256 checksum mismatch: verify archive checksum before extraction
instead of verifying extracted binary against archive hash (was always
failing). Add VerifyChecksum() and integration test.
- Fix JSON field name mismatch: TypeScript was emitting camelCase
(publishedAt, archiveFormat) but Go expected snake_case
(published_at, archive_format). Manifest parsing was silently broken.
- Add decompression size limit (256 MB) to prevent zip/gzip bombs.
- Don't update LastCheckedAt on transient errors so retry happens on
next CLI invocation instead of waiting 24h.
* feat: add PostHog usage analytics to CLI
Add anonymous command-level analytics to browseros-cli using the PostHog
Go SDK. Tracks which commands are executed, their success/failure status,
and duration — no PII or person profiles.
- New analytics package with Init/Track/Close singleton
- Distinct ID resolves from server's browseros_id (server.json), falls
back to CLI-generated UUID (~/.config/browseros-cli/install_id)
- API key injected at build time via ldflags (dev builds = silent no-op)
- Server now writes browseros_id into server.json for cross-surface
identity correlation
* fix: address PR review feedback for #603
- Return "unknown" for unrecognized args in commandName to avoid
sending arbitrary user input to PostHog
- Revert goreleaser to {{ .Env.POSTHOG_API_KEY }} (intentional hard
fail — release builds must have the key set)
- go mod tidy to fix posthog-go direct/indirect marker
- Add POSTHOG_API_KEY to .env.production.example
* feat: upload CLI binaries to CDN during release and gate workflow to core team
- Extend scripts/build/cli/upload.ts with uploadCliRelease() that pushes
archives + checksums to R2 under versioned (cli/v{VERSION}/) and latest
(cli/latest/) paths, plus a version.txt for lightweight latest resolution
- Update scripts/build/cli.ts entry point with --release/--version/--binaries-dir
flags (existing no-args behavior preserved for upload:cli-installers)
- Rewrite install.sh and install.ps1 to fetch from cdn.browseros.com instead of
GitHub releases API — eliminates rate limits and API dependency
- Add environment: release-core to release-cli.yml for core-team gating via
GitHub environment protection rules
- Add Bun setup + CDN upload step to the workflow between build and GitHub release
* fix: address review feedback for PR #602
- Make loadProdEnv return empty map when .env.production is absent so
pickEnv falls through to process.env in CI (Greptile P1)
- Add semver format validation for version string in install.sh and
install.ps1 to guard against malformed CDN responses
- Pass inputs.version via env var instead of inline ${{ }} interpolation
to prevent command injection in workflow shell