AI assistant for communities — React + Tauri v2 desktop app with a Rust core (JSON-RPC / CLI).
Narrative architecture: gitbooks/developing/architecture.md. Frontend: gitbooks/developing/architecture/frontend.md. Tauri shell: gitbooks/developing/architecture/tauri-shell.md. Agent-harness tool surface: gitbooks/developing/architecture/agent-harness.md.
| Path | Role |
|---|---|
app/ |
pnpm workspace openhuman-app (v0.53.45): Vite + React (app/src/), Tauri desktop host (app/src-tauri/), Vitest tests |
src/ (root) |
Rust lib crate openhuman + openhuman-core CLI binary (src/main.rs) — src/core/ (transport: Axum/HTTP, JSON-RPC, CLI), src/openhuman/* domains, event bus |
Cargo.toml (root) |
Core crate; cargo build --bin openhuman-core produces the binary. Also defines slack-backfill and gmail-backfill-3d helper binaries in src/bin/. |
docs/ |
Remaining deep internals (memory pipeline excalidraws, sentry, etc.). Public contributor docs live in gitbooks/developing/. |
Commands assume the repo root; pnpm dev delegates to the app workspace. The root package.json is openhuman-repo (private) and enforces pnpm via the packageManager field.
- Shipped product: desktop — Windows, macOS, Linux.
- Tauri host (
app/src-tauri): desktop-only. No Android/iOS branches. - Core runs in-process inside the Tauri host as a tokio task — there is no sidecar binary anymore (removed in PR #1061). The lifecycle is owned by
core_process::CoreProcessHandleinapp/src-tauri/src/core_process.rs; on Cmd+Q the core dies with the GUI. Frontend RPC still goes over HTTP (core_rpc_relay+core_rpcclient) tohttp://127.0.0.1:<port>/rpc, authenticated with a per-launch bearer inOPENHUMAN_CORE_TOKEN. SetOPENHUMAN_CORE_REUSE_EXISTING=1to attach to an externally-startedopenhuman-coreprocess (e.g. a debug harness).
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.
pnpm dev # Vite dev server only (app workspace)
pnpm dev:app # Full Tauri desktop dev (CEF runtime, loads env via scripts/load-dotenv.sh)
pnpm build # Production UI build
pnpm typecheck # tsc --noEmit (app workspace, aliased to `compile`)
pnpm compile # Same as typecheck
pnpm lint # ESLint --cache
pnpm format # Prettier write + cargo fmt
pnpm format:check # Prettier check + cargo fmt --check
# Rust — core library + CLI
cargo check --manifest-path Cargo.toml
cargo build --manifest-path Cargo.toml --bin openhuman-core
# Rust — Tauri shell
cargo check --manifest-path app/src-tauri/Cargo.toml
pnpm rust:check # Tauri shell checkNote: pnpm core:stage is a no-op (echoes a message). The sidecar was removed in PR #1061; core is linked in-process.
Tests: pnpm test (Vitest, app workspace) · pnpm test:coverage · pnpm test:rust (cargo test via scripts/test-rust-with-mock.sh).
Quality: ESLint + Prettier + Husky in app. Pre-push hook runs pnpm rust:check — pass --no-verify only for unrelated pre-existing breakage.
Bounded-output wrappers around the project test runners. Stdout stays summary-sized (so it fits in agent context); full output is teed to target/debug-logs/<kind>-<suffix>-<timestamp>.log. Add --verbose to also stream raw output. Prefer these over invoking Vitest / WDIO / cargo directly when iterating.
# Vitest
pnpm debug unit # full suite
pnpm debug unit src/components/Foo.test.tsx # one file (positional pattern)
pnpm debug unit -t "renders empty state" # filter by test name
pnpm debug unit Foo -t "renders empty" --verbose
# WDIO E2E (one spec at a time)
pnpm debug e2e test/e2e/specs/smoke.spec.ts
pnpm debug e2e test/e2e/specs/cron-jobs-flow.spec.ts cron-jobs --verbose
# cargo tests (delegates to scripts/test-rust-with-mock.sh)
pnpm debug rust
pnpm debug rust json_rpc_e2e
# Inspect saved logs
pnpm debug logs # list 50 most recent
pnpm debug logs last # print most recent (last 400 lines)
pnpm debug logs unit # most recent matching prefix "unit"
pnpm debug logs last --tail 100Files: scripts/debug/{cli,unit,e2e,rust,logs,lib}.sh plus README.md. Entry point is pnpm debug (scripts/debug/cli.sh).
PRs must meet ≥ 80% coverage on changed lines. Enforced by .github/workflows/coverage.yml using diff-cover over merged Vitest (app/coverage/lcov.info) and cargo-llvm-cov (core + Tauri shell) lcov outputs. Below the threshold the PR will not merge — add tests for new/changed lines, not just the happy path.
.env.example— Rust core, Tauri shell, backend URL, logging, proxy, storage, AI binary overrides. Load viasource scripts/load-dotenv.sh.app/.env.example—VITE_*(core RPC URL, backend URL, Sentry DSN, dev helpers). Copy toapp/.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).
- Co-locate as
*.test.ts/*.test.tsxunderapp/src/**. - Config:
app/test/vitest.config.ts; setup:app/src/test/setup.ts. - Run from repo root:
pnpm testorpnpm test:coverage. (Insideapp/,pnpm test:unitis also defined.) - Prefer behavior over implementation. Use helpers in
app/src/test/. No real network, no time flakes.
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:
pnpm mock:api.
Full guide: gitbooks/developing/e2e-testing.md.
- Linux (CI):
tauri-driver(WebDriver :4444). - macOS (local): Appium Mac2 (XCUITest :4723) on the
.appbundle. - Specs:
app/test/e2e/specs/*.spec.ts. Helpers inapp/test/e2e/helpers/. Config:app/test/wdio.conf.ts.
pnpm test:e2e:build
bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke
pnpm test:e2e:all:flows
docker compose -f e2e/docker-compose.yml run --rm e2e # Linux E2E on macOSUse element-helpers.ts (clickNativeButton, waitForWebView, clickToggle) — never raw XCUIElementType*. Assert UI outcomes and mock effects.
app/scripts/e2e-run-spec.sh creates and cleans a temp OPENHUMAN_WORKSPACE by default. OPENHUMAN_WORKSPACE redirects core config + storage away from ~/.openhuman. Each spec gets a fresh in-process core inside the freshly-built Tauri bundle.
pnpm test:rust
bash scripts/test-rust-with-mock.sh --test json_rpc_e2eProvider chain (App.tsx):
Sentry.ErrorBoundary → Redux Provider → PersistGate (with PersistRehydrationScreen) → BootCheckGate → CoreStateProvider → SocketProvider → ChatRuntimeProvider → HashRouter → CommandProvider → ServiceBlockingGate → AppShell (AppRoutes + BottomTabBar + walkthrough/mascot/snackbars).
No UserProvider / AIProvider / SkillProvider — auth and core snapshot live in CoreStateProvider, fetched via fetchCoreAppSnapshot() RPC (auth tokens are NOT in redux-persist; they live in the in-process core).
State (store/): Redux Toolkit slices — accounts, channelConnections, chatRuntime, coreMode, deepLinkAuth, mascot, notification, providerSurface, socket, thread. Persisted slices via redux-persist. Prefer Redux over ad-hoc localStorage (exception: ephemeral UI state like upsell dismiss flags).
Services (services/): singletons — apiClient, socketService, coreRpcClient + coreCommandClient (HTTP bridge to in-process core via Tauri IPC), chatService, analytics, notificationService, webviewAccountService, daemonHealthService, plus domain api/* clients.
MCP (lib/mcp/): JSON-RPC transport, validation, types over Socket.io.
Routing (AppRoutes.tsx, HashRouter): / (Welcome), /onboarding/*, /home, /human, /intelligence, /skills, /chat (unified agent + connected web apps, replaces old /conversations + /accounts), /channels, /invites, /notifications, /rewards, /webhooks (redirects to /settings/webhooks-triggers), /settings/*. Default catch-all is DefaultRedirect. There is no /login, no /mnemonic (recovery phrase moved to Settings), no /agents, no /conversations.
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.
Thin desktop host. Top-level modules: core_process, core_rpc, cdp, cef_preflight, cef_profile, dictation_hotkeys, file_logging, mascot_native_window, native_notifications, notification_settings, process_kill, process_recovery, screen_capture, window_state, plus the per-provider scanner modules (discord_scanner, gmessages_scanner, imessage_scanner, meet_scanner, slack_scanner, telegram_scanner, whatsapp_scanner), meet_audio / meet_call / meet_video, fake_camera, webview_accounts, webview_apis.
Core lifecycle: core_process::CoreProcessHandle spawns the JSON-RPC server as an in-process tokio task and authenticates inbound RPC with a per-launch hex bearer (OPENHUMAN_CORE_TOKEN). On stale-listener detection (#1130) the handle revalidates the PID before force-killing so PID reuse can't kill an unrelated process. restart_core_process / start_core_process Tauri commands let the frontend cycle it for updates.
Registered IPC (see gitbooks/developing/architecture/tauri-shell.md) includes greet, write_ai_config_file, ai_get_config, ai_refresh_config, core_rpc_relay, core_rpc_token, start_core_process, restart_core_process, window commands, and openhuman_* daemon helpers. Always use invoke('core_rpc_relay', ...) for in-process RPC (avoids CORS preflight that fetch() would trigger).
Embedded provider webviews (acct_*, loading third-party origins like web.telegram.org, linkedin.com, slack.com, …) must not grow any new JavaScript injection. Do not add new .js files under app/src-tauri/src/webview_accounts/, do not append new blocks to build_init_script / RUNTIME_JS, and do not dispatch scripts via CDP Page.addScriptToEvaluateOnNewDocument / Runtime.evaluate for these webviews. The migrated providers (whatsapp, telegram, slack, discord, browserscan) load with zero injected JS under CEF by design — all scraping and observability runs natively via CDP in the per-provider scanner modules, and anything host-controlled that runs inside a third-party origin is a scraping/attack-surface liability.
New behavior for these webviews lives in:
- CEF handlers —
on_navigation,on_new_window,LoadHandler::OnLoadStart,CefRequestHandler::*(wired inwebview_accounts/mod.rs). - CDP from the scanner side —
Network.*,Emulation.*,Input.*,Page.*driven by the per-provider*_scanner/modules. - Rust-side notification/IPC hooks — never cross into the renderer.
If a feature truly cannot be built this way (e.g. intercepting a click the page's JS preventDefaults), the correct answer is to surface the limitation, not to ship an init script. Legacy injection that already exists for non-migrated providers (gmail, linkedin, google-meet recipe files plus the runtime.js bridge) is grandfathered but should shrink, not grow.
Watch out for Tauri plugins that inject JS by default. tauri-plugin-opener ships init-iife.js (a global click listener that calls plugin:opener|open_url via HTTP-IPC) unless you build it with .open_js_links_on_click(false). Any new plugin added to app/src-tauri/src/lib.rs must be audited for a js_init_script call — if found, opt out or configure around it.
src/openhuman/— Domain logic. Current domains:about_app,accessibility,agent,app_state,approval,autocomplete,billing,channels,composio,config,context,cost,credentials,cron,doctor,embeddings,encryption,health,heartbeat,integrations,learning,local_ai,meet,meet_agent,memory,migration,node_runtime,notifications,overlay,people,prompt_injection,provider_surfaces,providers,redirect_links,referral,routing,scheduler_gate,screen_intelligence,security,service,skills,socket,subconscious,team,text_input,threads,tokenjuice,tool_timeout,tools,tree_summarizer,update,voice,wallet,webhooks,webview_accounts,webview_apis,webview_notifications. RPC controllers in per-domainrpc.rs/schemas.rs; useRpcOutcome<T>perAGENTS.md.- Skills runtime removed: the QuickJS /
rquickjsruntime that previously executed skill packages is gone.src/openhuman/skills/is now a metadata-only domain (ops_create,ops_discover,ops_install,ops_parse,inject,schemas,types) — see the module header comment "Legacy skill metadata helpers retained after QuickJS runtime removal." - Module layout rule: new functionality goes in a dedicated subdirectory (
openhuman/<domain>/mod.rs+ siblings). Do not add new standalone*.rsfiles atsrc/openhuman/root (dev_paths.rsandutil.rsare grandfathered, not a template). - Controller schema contract: shared types in
src/core/types.rs/src/core/mod.rs(ControllerSchema,FieldSchema,TypeSchema). - Domain schema files: per-domain
schemas.rs(e.g.src/openhuman/cron/schemas.rs), exported from domainmod.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 domainmod.rsexport-focused. Operational code inops.rs,store.rs,types.rs, etc. src/core/— Transport only. Modules:all,all_tests,auth,autocomplete_cli_adapter,cli,cli_tests,dispatch,event_bus/,jsonrpc,jsonrpc_tests,legacy_aliases,logging,memory_cli,observability,rpc_log,shutdown,socketio,types, plusagent_cli. No heavy domain logic here. (There is nosrc/core_server/— older docs that referencecore_servermeansrc/core/.)
src/openhuman/<domain>/mod.rs: addmod schemas;, re-exportall_controller_schemas as all_<domain>_controller_schemasandall_registered_controllers as all_<domain>_registered_controllers.src/openhuman/<domain>/schemas.rsdefinesschemas,all_controller_schemas,all_registered_controllers, andhandle_*fns delegating to domainrpc.rs.- Wire exports into
src/core/all.rs. Remove migrated branches fromsrc/core/dispatch.rs.
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::Senderpass through unchanged. Internal-only; JSON-RPC-facing work goes throughsrc/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.
Premium, calm visual language — ocean primary #4A83DD, sage / amber / coral semantics, Inter + Cabinet Grotesk + JetBrains Mono, Tailwind with custom radii/spacing/shadows. Implementation tokens live in app/tailwind.config.js.
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.
- Never write code on
main. Before making any code changes, fork a new branch off the latestmain(git fetch upstream && git checkout -b <branch> upstream/main). All work happens on that feature branch;mainstays clean and only advances via merged PRs. - Issues and PRs on upstream tinyhumansai/openhuman — not a fork — unless explicitly told otherwise.
- Issue templates:
.github/ISSUE_TEMPLATE/feature.md,.github/ISSUE_TEMPLATE/bug.md. PR template:.github/PULL_REQUEST_TEMPLATE.md. AI-authored text should follow them verbatim. - PRs target
main. - Push branches to
origin(the user's fork —senamakel/openhuman), never toupstream(tinyhumansai/openhuman). PRs are still opened againsttinyhumansai/openhuman:main, but with--head senamakel:<branch>so the source is the fork. Direct pushes to upstream pollute its branch list and skip code-review boundaries. Treat theupstreamremote as fetch-only. - When the user asks you to push or open a PR, resolve blockers and push — don't prompt for permission. If a pre-push hook fails on something unrelated to your changes (e.g. pre-existing breakage on
mainin code you didn't touch), push with--no-verifyand call it out in the PR body. If the hook fails on your own changes, fix them and push again. Don't ask the user whether to bypass — just do the right thing and tell them what you did.
- 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.mdor architecture docs when rules or user-visible behavior change.
- 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/tracingatdebug/trace.app/: namespaceddebug+ dev-only detail. - Never log secrets or full PII — redact.
- Changes lacking diagnosis logging are incomplete.
Specify → prove in Rust → prove over RPC → surface in the UI → test.
- Specify against the current codebase — ground in existing domains, controller/registry patterns, JSON-RPC naming (
openhuman.<namespace>_<function>). No parallel architectures. - Implement in Rust — domain logic under
src/openhuman/<domain>/, schemas + handlers in the registry, unit tests until correct in isolation. - JSON-RPC E2E — extend
tests/json_rpc_e2e.rs/scripts/test-rust-with-mock.shso RPC methods match what the UI will call. - UI in Tauri app — React screens/state using
core_rpc_relay/coreRpcClient. Keep rules in the core. - App unit tests — Vitest.
- 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.
- File size: prefer ≤ ~500 lines; split growing modules.
- Pre-merge (code changes): Prettier, ESLint,
tsc --noEmitinapp/;cargo fmt+cargo checkfor changed Rust. - No dynamic imports in production
app/srccode — staticimport/import typeonly. Noimport(),React.lazy(() => import(...)),await import(...). For heavy optional paths, use a static import and guard the call site withtry/catchor a runtime check. Exceptions: Vitest harness patterns in*.test.ts/__tests__/test/setup.ts; ambienttypeof import('…')in.d.ts; config files (e.g.tailwind.config.jsJSDoc). - Dual socket sync: when changing the realtime protocol, keep
socketService/ MCP transport aligned with core socket behavior (seegitbooks/developing/architecture.mddual-socket section).
- Vendored CEF-aware
tauri-cli: runtime is CEF; only the vendored CLI atapp/src-tauri/vendor/tauri-cef/crates/tauri-clibundles Chromium intoContents/Frameworks/. Stock@tauri-apps/cliproduces a broken bundle (panic incef::library_loader::LibraryLoader::new).pnpm dev:appand allcargo tauriscripts callpnpm tauri:ensurewhich runsscripts/ensure-tauri-cli.sh. If overwritten, reinstall withcargo install --locked --path app/src-tauri/vendor/tauri-cef/crates/tauri-cli. - macOS deep links: often require a built
.appbundle, not justtauri dev. - Tauri environment guard: use
isTauri()(fromapp/src/services/webviewAccountService.ts) or wrapinvoke(...)intry/catch; do not checkwindow.__TAURI__directly — it is not present at module load and bypasses the established wrapper contract. - Core is in-process (no sidecar):
core_rpcreaches the embedded server athttp://127.0.0.1:<port>/rpcwith bearer auth viaOPENHUMAN_CORE_TOKEN.scripts/stage-core-sidecar.mjsno longer exists;pnpm core:stageis a no-op echo. To run the core standalone for debugging, use./target/debug/openhuman-core serve(token at{workspace}/core.token, default~/.openhuman-staging/core.tokenunderOPENHUMAN_APP_ENV=staging).