Files
openhuman/CLAUDE.md
T
3bb714bf96 feat(webview): native OS notifications from embedded webview apps (#714) (#727)
* feat(webview_accounts): native OS notifications from embedded webviews (#714)

Forward CEF notification intercept payloads to tauri-plugin-notification,
prefixing the title with the provider label so the source of each toast is
obvious at a glance. Honour `silent` (skip toast, still record route),
`icon` (passed through to the native builder), and `tag` (used as the
dedup key, with a monotonic timestamp fallback for untagged payloads).

Record a NotificationRoute keyed by `{provider}:{account_id}:{tag_or_uuid}`
so a future click hook (UNUserNotificationCenter / notify-rust on_response)
can route the OS click back to the source account. Entries are cleared on
webview_account_close / _purge to bound map growth.

Expose webview_notification_permission_state / _request commands mapping
tauri::plugin::PermissionState onto the web API triple. Non-cef stubs
return "default" so the frontend can call the same invoke names on both
runtimes. Wire notification:allow-* capabilities so the plugin can be
invoked from the webview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(accounts): wire notification permission + click bridge (#714)

Round-trip the OS notification permission once per session on first
account open via the new invoke pair. Attach a dormant notification:click
listener that dispatches setActiveAccount and brings the main window to
front when a platform click hook starts emitting the event — contract
matches the Rust NotificationRoute shape so the emit side is a one-liner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: sync Cargo.lock to 0.52.26 after version bump

Lockfile picked up the pending 0.52.26 version bump from Cargo.toml
while building the notification feature. No dependency graph change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(notifications): add notification bypass for embedded webview apps (#679)

- Add NotificationBypassPrefs (global DND, per-account mute, bypass-when-focused)
  to WebviewAccountsState with thread-safe AtomicBool window focus tracking
- Evaluate all three bypass conditions inside forward_native_notification before
  showing OS toast; each suppression path logs at debug with [notify-bypass] prefix
- Add four new Tauri commands: webview_notification_set_dnd,
  webview_notification_mute_account, webview_notification_get_bypass_prefs,
  webview_set_focused_account
- Wire window focus tracking in setup hook via on_window_event Focused handler
- Frontend: add setAccountMuted, setGlobalDnd, getBypassPrefs, setFocusedAccount
  helpers in webviewAccountService; sync focused account on open + click
- Add NotificationsPanel settings page with Global DND toggle
- Register NotificationsPanel at /settings/notifications

Closes #679

* feat(notifications): integrate notifications feature into app

- Added Notifications page and routing to AppRoutes.
- Introduced NotificationRoutingPanel in Settings for managing notification settings.
- Updated SettingsHome to include navigation for notification routing.
- Integrated notifications reducer into the store for state management.
- Enhanced Rust backend to support notification handling from embedded webviews.

This commit lays the groundwork for a comprehensive notification system within the application.

* refactor(notifications): clean up code formatting and structure

- Simplified JSX structure in NotificationCard for better readability.
- Consolidated fetchNotifications call in NotificationCenter for cleaner syntax.
- Improved formatting in NotificationRoutingPanel and notificationsSlice for consistency.
- Enhanced Rust code readability by streamlining function signatures and logic.

These changes enhance code maintainability and readability across the notifications feature.

* refactor(webview_accounts): simplify webview_notification_set_dnd function signature

- Removed unnecessary line breaks in the webview_notification_set_dnd function for improved readability.

* feat(notifications): implement provider-level notification settings management

- Added `getNotificationSettings` and `setNotificationSettings` functions to manage notification settings for providers.
- Enhanced `NotificationRoutingPanel` to display and update settings for Gmail, Slack, Discord, and WhatsApp.
- Introduced new RPC endpoints for retrieving and updating notification settings.
- Updated database schema to store notification settings persistently.

This commit establishes a robust system for managing notification preferences, improving user control over notifications.

* refactor(notifications): improve code formatting and readability

- Enhanced formatting in NotificationRoutingPanel for better clarity.
- Streamlined function signatures in notificationService and Rust backend.
- Improved readability of assertions in tests by adjusting line breaks.

These changes contribute to a more maintainable and comprehensible codebase for the notifications feature.

* chore(vendor): bump tauri-cef to fix Slack notification permission banner

Updates the tauri-cef submodule to 55db2d6 which adds a
navigator.permissions.query shim in the CEF render process. Slack checks
this API (not just Notification.permission) to decide whether to show its
"needs your permission" banner — the shim returns "granted" for
notifications queries so the banner no longer appears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(vendor): bump tauri-cef for cargo fmt fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(vendor): bump tauri-cef — native V8 permissions.query shim

Switches from context.eval() to a proper PermissionsQueryV8Handler so
the navigator.permissions.query fix actually runs in on_context_created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(notifications): patch navigator.permissions.query in ua_spoof.js

The V8 set_value_bykey approach in cef-helper's on_context_created does
not stick on CEF platform objects (Chromium's V8 binding layer silently
ignores property writes on native wrappers like Permissions). The init
script path via frame.execute_java_script runs in the fully-initialised
JS context where navigator.permissions IS writable, matching how
ua_spoof.js already overrides navigator.userAgent successfully.

Slack checks navigator.permissions.query({ name: 'notifications' })
before showing its "needs permission" banner — patching it here to
return "granted" removes the banner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(notifications): use Object.defineProperty to shim navigator.permissions

Two-layer fix for the Slack "needs permission to enable notifications" banner:

1. cef-helper (submodule update to 99a2686): context.eval() in
   on_context_created installs Object.defineProperty(navigator, 'permissions',
   ...) before any page JS runs.

2. ua_spoof.js: same Object.defineProperty pattern as belt-and-suspenders
   for frames that reload or trigger permission checks after on_load_end.

Simple property assignment on Blink platform objects is silently ignored;
Object.defineProperty on the navigator wrapper itself (the same mechanism
already used for navigator.userAgent) works correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(notifications): address CodeRabbit review issues on PR #727

- Move raw_title out of log::info! (PII risk) — log title_chars at info,
  raw_title at debug only
- Fix permissionChecked set before async invoke in ensureNotificationPermission
  so transient failures allow retry on next account open

* fix(cef): enable webview-data-url feature for CEF placeholder URL

The CEF backend uses a data: URL as the initial webview location so CDP
can attach before the real provider URL loads. Tauri's add_child rejects
data: URLs unless the webview-data-url feature is enabled.

* fix(notifications): address remaining CodeRabbit issues on PR #727

- cdp/emulation: bump Chrome UA 124→136 to pass Slack browser check
- cdp/session: inject Page.addScriptToEvaluateOnNewDocument to stub
  Notification.permission as "granted" and silence provider banners
- notifications/mod.rs + core/all.rs: wire notifications domain into
  the controller registry (fixes unknown-method in json_rpc_e2e tests)
- notifications/schemas: add skipped bool output to ingest schema
- notifications/store: add tracing::warn on datetime parse failure
- notificationService: union return type for ingestNotification
- webviewAccountService: narrow union before accessing result.id
- NotificationCenter: drive loading/error from fetch effect; track
  allProviders separately so filter pills don't collapse on selection
- NotificationRoutingPanel: rollback optimistic update on save failure
- useSettingsNavigation: add notifications/notification-routing routes
- scripts/install.sh: remove silent dry-run exit 0 on asset failure
- scripts/setup-dev-codesign.sh: remove unconditional -legacy flag
- docs/SUMMARY.md: remove worktree path, fix macOS capitalisation,
  remove self-referential deletion note

* chore: apply prettier + cargo fmt + fix useEffect dep warning

Auto-apply formatting changes flagged by the pre-push hook:
- prettier reformatted NotificationCenter.tsx and notificationService.ts
- cargo fmt reformatted all.rs, openhuman/mod.rs, notifications/schemas.rs
- NotificationRoutingPanel: move providers array to module scope so
  useEffect dependency array is satisfied without exhaustive-deps warning

* feat(notifications): enhance notification management and permissions

- Added new commands for managing notification preferences, including setting global Do Not Disturb (DND), muting specific accounts, and retrieving current bypass preferences.
- Implemented a notification permission state handler to ensure consistent behavior across different environments.
- Updated the JavaScript shim for notification permissions to handle both Notification and PushManager states, ensuring compatibility with various providers.
- Refactored the WebviewAccountsState to include a new structure for managing notification bypass preferences, improving the overall notification handling logic.

* update agents

* fix(notifications): complete schema + navigation metadata for ingest/settings routes

- app/src/components/settings/hooks/useSettingsNavigation.ts: resolve the
  new `/settings/notifications` and `/settings/notification-routing` URLs
  to their SettingsRoute values and feed them into breadcrumbs so the new
  panels don't silently fall through to `'home'`. Addresses CodeRabbit on
  useSettingsNavigation.ts:34.
- src/openhuman/notifications/schemas.rs: add the optional `reason` output
  on `notification.ingest` (populated alongside `skipped=true` by the
  runtime) and the normalized `settings` output on `notification.settings_set`
  so schema-driven clients see the full response shape. Addresses
  CodeRabbit on schemas.rs:103 and schemas.rs:217.
- src/core/all.rs: add a `notification` namespace_description so CLI help
  covers the new controllers, plus a test assertion. Addresses CodeRabbit
  on src/core/all.rs:149.

* fix(notifications): trace DB entry, surface empty update matches, warn on bad scored_at

- Add `tracing::trace!` checkpoints around the `with_connection` DB open
  and schema migration so notification-delivery issues are reconstructible
  from logs.
- `update_triage` and `mark_read` now inspect `Connection::execute`'s
  affected-row count: log a `warn!` when the update matched zero rows
  (row deleted between ingest and scoring / client passed a stale id),
  `debug!` on the normal path.
- `scored_at` parsing no longer silently drops malformed values — log a
  `warn!` with the raw value and parse error before treating the row as
  unscored, matching the existing behavior for `received_at`.

Addresses CodeRabbit on store.rs (lines 72, 172, 294).

* fix(webview): respect silent notifications, multi-host CDP fallback, shim idempotency

- webview_accounts/mod.rs: honor the Web Notification `silent` flag.
  Previously we only logged it and still called `builder.show()`, so
  pages that marked a notification silent still produced an OS toast.
  Mirror event still fires so the in-app center updates; only the OS
  toast is suppressed. Also picks up a prior cargo-fmt rewrap.
- cdp/target.rs: `browser_ws_url()` now continues the host loop when
  `resp.json()` fails instead of early-returning via `?`. A malformed
  response from the first host (CDP_HOST) no longer prevents the
  `localhost` fallback from being tried.
- webview_accounts/ua_spoof.js: guard the Notification wrapper behind
  `window.__OH_NOTIF_SHIM` so repeated evaluations of the script
  (Page.addScriptToEvaluateOnNewDocument + frame-level re-injections)
  don't stack wrappers onto the same page globals or re-proxy
  `Function.prototype.toString`.

Addresses CodeRabbit on webview_accounts/mod.rs:377, cdp/target.rs:33,
and ua_spoof.js:176.

* update agents

* update

---------

Co-authored-by: oxoxDev <nikhil@tinyhumans.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
2026-04-22 14:13:35 -07:00

14 KiB

OpenHuman

AI assistant for communities — React + Tauri v2 desktop app with a Rust core (JSON-RPC / CLI).

Narrative architecture: docs/ARCHITECTURE.md. Frontend: docs/src/README.md. Tauri shell: docs/src-tauri/README.md.


Repository layout

Path Role
app/ Yarn workspace openhuman-app: Vite + React (app/src/), Tauri desktop host (app/src-tauri/), Vitest tests
src/ (root) Rust lib openhuman_core + openhuman CLI binary — core_server, openhuman::* domains, MCP routing
Cargo.toml (root) Core crate; cargo build --bin openhuman produces the sidecar staged by app's core:stage
docs/ Architecture and module guides

Commands assume the repo root; yarn dev delegates to the app workspace.


Runtime scope

  • Shipped product: desktop — Windows, macOS, Linux.
  • Tauri host (app/src-tauri): desktop-only (compile_error! for other targets). No Android/iOS branches.
  • Core binary (openhuman): spawned as a sidecar; the UI talks to it over HTTP (core_rpc_relay + core_rpc client), not by duplicating domain logic.

Where logic lives

  • Rust core: business logic, execution, domains, RPC, persistence, CLI. Authoritative.
  • Tauri + React (app/): UX, screens, navigation, bridging to the core. Presents and orchestrates only.

Commands (from repo root)

yarn dev                  # Frontend + Tauri dev
yarn tauri dev            # Desktop with Tauri (loads env via scripts/load-dotenv.sh)
yarn build                # Production UI build
yarn typecheck            # Typecheck (app workspace)
yarn lint                 # ESLint
yarn format               # Prettier write
yarn format:check         # Prettier check
cd app && yarn core:stage # Stage openhuman binary next to Tauri resources

# Rust — core library + CLI
cargo check --manifest-path Cargo.toml
cargo build --manifest-path Cargo.toml --bin openhuman

# Rust — Tauri shell
cargo check --manifest-path app/src-tauri/Cargo.toml

Tests: Vitest in app/ (yarn test:unit, yarn test:coverage); Rust via cargo test. Quality: ESLint + Prettier + Husky in app.


Configuration

  • .env.example — Rust core, Tauri shell, backend URL, logging, proxy, storage, AI binary overrides. Load via source scripts/load-dotenv.sh.
  • app/.env.exampleVITE_* (core RPC URL, backend URL, Sentry DSN, dev helpers). Copy to app/.env.local.

Frontend config is centralized in app/src/utils/config.ts. Read VITE_* there and re-export — never import.meta.env directly elsewhere.

Rust config uses a TOML Config struct (src/openhuman/config/schema/types.rs) with env overrides (src/openhuman/config/schema/load.rs).


Testing

Unit (Vitest)

  • Co-locate as *.test.ts / *.test.tsx under app/src/**.
  • Config: app/test/vitest.config.ts; setup: app/src/test/setup.ts.
  • Run: yarn test:unit, yarn test:coverage.
  • Prefer behavior over implementation. Use helpers in app/src/test/. No real network, no time flakes.

Shared mock backend

Used by both unit and Rust tests.

  • Core: scripts/mock-api-core.mjs · server: scripts/mock-api-server.mjs · E2E wrapper: app/test/e2e/mock-server.ts.
  • Admin: GET /__admin/health, POST /__admin/reset, POST /__admin/behavior, GET /__admin/requests.
  • Run manually: yarn mock:api.

E2E (WDIO — dual platform)

Full guide: docs/E2E-TESTING.md.

  • Linux (CI): tauri-driver (WebDriver :4444).
  • macOS (local): Appium Mac2 (XCUITest :4723) on the .app bundle.
  • Specs: app/test/e2e/specs/*.spec.ts. Helpers in app/test/e2e/helpers/. Config: app/test/wdio.conf.ts.
yarn test:e2e:build
bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke
yarn test:e2e:all:flows
docker compose -f e2e/docker-compose.yml run --rm e2e   # Linux E2E on macOS

Use element-helpers.ts (clickNativeButton, waitForWebView, clickToggle) — never raw XCUIElementType*. Assert UI outcomes and mock effects.

Deterministic core-sidecar reset

app/scripts/e2e-run-spec.sh creates and cleans a temp OPENHUMAN_WORKSPACE by default. OPENHUMAN_WORKSPACE redirects core config + storage away from ~/.openhuman.

Rust tests with mock

yarn test:rust
bash scripts/test-rust-with-mock.sh --test json_rpc_e2e

Frontend (app/src/)

Provider chain (App.tsx): ReduxPersistGateUserProviderSocketProviderAIProviderSkillProviderHashRouterAppRoutes.

State (store/): Redux Toolkit slices — auth, user, socket, ai, skills, team, etc. Prefer Redux (persisted where configured) over ad-hoc localStorage.

Services (services/): singletons — apiClient, socketService, coreRpcClient (HTTP bridge to core), domain api/* clients.

MCP (lib/mcp/): JSON-RPC transport, validation, types over Socket.io. Tooling is driven by the backend + skills system.

Routing (AppRoutes.tsx): hash routes /, /onboarding, /mnemonic, /home, /intelligence, /skills, /conversations, /invites, /agents, /settings/*. No /login.

AI config: bundled prompts in src/openhuman/agent/prompts/ (also bundled via app/src-tauri/tauri.conf.json resources). Loaders in app/src/lib/ai/ use ?raw imports, optional remote fetch, and ai_get_config / ai_refresh_config in Tauri.


Tauri shell (app/src-tauri/)

Thin desktop host: window management, daemon health, core process lifecycle (core_process, CoreProcessHandle), JSON-RPC relay (core_rpc_relay, core_rpc).

Registered IPC (see docs/src-tauri/02-commands.md): greet, write_ai_config_file, ai_get_config, ai_refresh_config, core_rpc_relay, window commands, openhuman_* daemon helpers.


Rust core (src/)

  • openhuman/ — Domain logic (memory, channels, config, cron, skills, webhooks, …). RPC controllers in per-domain rpc.rs; use RpcOutcome<T> per AGENTS.md.
  • Module layout rule: new functionality goes in a dedicated subdirectory (openhuman/<domain>/mod.rs + siblings). Do not add new standalone *.rs files at src/openhuman/ root.
  • Controller schema contract: shared types in src/core/mod.rs (ControllerSchema, FieldSchema, TypeSchema).
  • Domain schema files: per-domain schemas.rs (e.g. src/openhuman/cron/schemas.rs), exported from domain mod.rs.
  • Controller-only exposure: expose features to CLI and JSON-RPC via the controller registry. Do not add domain branches in src/core/cli.rs / src/core/jsonrpc.rs.
  • Light mod.rs: keep domain mod.rs export-focused. Operational code in ops.rs, store.rs, types.rs, etc.
  • core_server/ — Transport only: Axum/HTTP, JSON-RPC envelope, CLI parsing, dispatch. No heavy logic.

Controller migration checklist

  • src/openhuman/<domain>/mod.rs: add mod schemas;, re-export all_controller_schemas as all_<domain>_controller_schemas and all_registered_controllers as all_<domain>_registered_controllers.
  • src/openhuman/<domain>/schemas.rs defines schemas, all_controller_schemas, all_registered_controllers, and handle_* fns delegating to domain rpc.rs.
  • Wire exports into src/core/all.rs. Remove migrated branches from src/rpc/dispatch.rs.

Event bus (src/core/event_bus/)

Typed pub/sub + in-process typed request/response. Both singletons — use module-level functions; never construct EventBus / NativeRegistry directly.

  • Broadcast (publish_global / subscribe_global) — fire-and-forget. Many subscribers, no return.
  • Native request/response (register_native_global / request_native_global) — one-to-one typed dispatch keyed by method string. Zero serialization — trait objects, mpsc::Sender, oneshot::Sender pass through unchanged. Internal-only; JSON-RPC-facing work goes through src/core/all.rs.

Core types (all in src/core/event_bus/):

Type File Purpose
DomainEvent events.rs #[non_exhaustive] enum of all cross-module events
EventBus bus.rs Singleton over tokio::sync::broadcast; ctor is pub(crate)
NativeRegistry / NativeRequestError native_request.rs Typed request/response registry by method name
EventHandler subscriber.rs Async trait with optional domains() filter
SubscriptionHandle subscriber.rs RAII — drops cancel the subscriber
TracingSubscriber tracing.rs Built-in debug logger

Singleton API: init_global(capacity), publish_global(event), subscribe_global(handler), register_native_global(method, handler), request_native_global(method, req), global() / native_registry().

Domains: agent, memory, channel, cron, skill, tool, webhook, system.

Each domain owns a bus.rs with its EventHandler impls — e.g. cron/bus.rs (CronDeliverySubscriber), webhooks/bus.rs (WebhookRequestSubscriber), channels/bus.rs (ChannelInboundSubscriber). Convention: <Purpose>Subscriber + name() returning "<domain>::<purpose>".

Adding events: add variants to DomainEvent, extend the domain() match, create <domain>/bus.rs, register subscribers at startup, publish via publish_global.

Adding a native handler: define request/response types in the domain (owned fields, Arcs, channels — not borrows; Send + 'static, not Serialize). Register at startup keyed by "<domain>.<verb>". Callers dispatch via request_native_global.

Tests: re-register the same method to override; or construct a fresh NativeRegistry::new() for isolation.


Design

Premium, calm visual language — ocean primary #4A83DD, sage / amber / coral semantics, Inter + Cabinet Grotesk + JetBrains Mono, Tailwind with custom radii/spacing/shadows. See docs/DESIGN_GUIDELINES.md.

Shell vs app code

Tauri/Rust in the shell is a delivery vehicle (windowing, process lifecycle, IPC). Keep UI behavior and product logic in TypeScript/React (app/). Only grow Rust in the shell for hard platform/security reasons.

Git workflow


Coding philosophy

  • Unix-style modules: small, sharp-responsibility units composed through clear boundaries.
  • Tests before the next layer: ship unit tests for new/changed behavior before stacking features. Untested code is incomplete.
  • Docs with code: new/changed behavior ships with matching rustdoc / code comments; update AGENTS.md or architecture docs when rules or user-visible behavior change.

Debug logging (must follow)

  • Default to verbose diagnostics on new/changed flows so issues are easy to trace end-to-end.
  • Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors.
  • Use stable grep-friendly prefixes ([domain], [rpc], [ui-flow]) and correlation fields (request IDs, method names, entity IDs).
  • Rust: log / tracing at debug / trace. app/: namespaced debug + dev-only detail.
  • Never log secrets or full PII — redact.
  • Changes lacking diagnosis logging are incomplete.

Feature design workflow

Specify → prove in Rust → prove over RPC → surface in the UI → test.

  1. Specify against the current codebase — ground in existing domains, controller/registry patterns, JSON-RPC naming (openhuman.<namespace>_<function>). No parallel architectures.
  2. Implement in Rust — domain logic under src/openhuman/<domain>/, schemas + handlers in the registry, unit tests until correct in isolation.
  3. JSON-RPC E2E — extend tests/json_rpc_e2e.rs / scripts/test-rust-with-mock.sh so RPC methods match what the UI will call.
  4. UI in Tauri app — React screens/state using core_rpc_relay / coreRpcClient. Keep rules in the core.
  5. App unit tests — Vitest.
  6. App E2E — desktop specs for user-visible flows.

Capability catalog: when a change adds/removes/renames a user-facing feature, update src/openhuman/about_app/ in the same work.

Planning rule: up front, define the E2E scenarios (core RPC + app) that cover the full intended scope — happy paths, failure modes, auth gates, regressions. Not testable end-to-end ⇒ incomplete spec or too-large cut.


Key patterns

  • File size: prefer ≤ ~500 lines; split growing modules.
  • Pre-merge (code changes): Prettier, ESLint, tsc --noEmit in app/; cargo fmt + cargo check for changed Rust.
  • No dynamic imports in production app/src code — static import / import type only. No import(), React.lazy(() => import(...)), await import(...). For heavy optional paths, use a static import and guard the call site with try/catch or a runtime check. Exceptions: Vitest harness patterns in *.test.ts / __tests__ / test/setup.ts; ambient typeof import('…') in .d.ts; config files (e.g. tailwind.config.js JSDoc).
  • Dual socket sync: when changing the realtime protocol, keep socketService / MCP transport aligned with core socket behavior (see docs/ARCHITECTURE.md dual-socket section).

Platform notes

  • Vendored CEF-aware tauri-cli: runtime is CEF; only the vendored CLI at app/src-tauri/vendor/tauri-cef/crates/tauri-cli bundles Chromium into Contents/Frameworks/. Stock @tauri-apps/cli produces a broken bundle (panic in cef::library_loader::LibraryLoader::new). yarn dev:app and all cargo tauri scripts call yarn tauri:ensure which runs scripts/ensure-tauri-cli.sh. If overwritten, reinstall with cargo install --locked --path app/src-tauri/vendor/tauri-cef/crates/tauri-cli.
  • macOS deep links: often require a built .app bundle, not just tauri dev.
  • window.__TAURI__: not present at module load; guard Tauri usage.
  • Core sidecar: must be staged so core_rpc can reach the openhuman binary (see scripts/stage-core-sidecar.mjs).