Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ OPENHUMAN_CORE_RPC_URL=http://127.0.0.1:7788/rpc
# Set this when serving a private web UI preview from a non-loopback origin.
# OPENHUMAN_CORE_ALLOWED_ORIGINS=https://openhuman-ui.example.com
# Core RPC bearer token. Single source of truth for /rpc auth.
# - Tauri desktop: set automatically by the shell — leave blank.
# - Tauri desktop: leave blank. The shell generates a fresh per-launch
# bearer and hands it to the in-process core in-memory; nothing is
# read from this variable.
# - Docker / cloud / VPS: REQUIRED. Generate with `openssl rand -hex 32`.
# Same value goes in the desktop's app/.env.local (or paste into the
# first-run picker). See gitbooks/features/cloud-deploy.md.
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Commands in documentation assume the **repo root** unless noted: `pnpm dev` runs

- **Shipped product**: desktop — Windows, macOS, Linux (see [`gitbooks/developing/architecture.md`](gitbooks/developing/architecture.md) "Platform reach").
- **Tauri host** (`app/src-tauri`): **desktop-only**. Do not add Android/iOS branches.
- **Core runs in-process** as a tokio task inside the Tauri host (sidecar removed in PR #1061). The host owns its lifetime via `core_process::CoreProcessHandle` in `app/src-tauri/src/core_process.rs`. Frontend RPC still goes over HTTP to `http://127.0.0.1:<port>/rpc` authenticated with a per-launch hex bearer in `OPENHUMAN_CORE_TOKEN`; the Tauri command `core_rpc_token` exposes it to the renderer. Set `OPENHUMAN_CORE_REUSE_EXISTING=1` to attach to an externally-started `openhuman-core` process for debugging.
- **Core runs in-process** as a tokio task inside the Tauri host (sidecar removed in PR #1061). The host owns its lifetime via `core_process::CoreProcessHandle` in `app/src-tauri/src/core_process.rs`. Frontend RPC still goes over HTTP to `http://127.0.0.1:<port>/rpc` authenticated with a per-launch hex bearer; the Tauri shell generates it in `CoreProcessHandle::new()` and hands it to the embedded server in-memory via `run_server_embedded_with_ready(rpc_token: Some(_))` — `OPENHUMAN_CORE_TOKEN` is no longer set on the process env by the desktop shell (CLI / docker / cloud env-as-config is preserved). The Tauri command `core_rpc_token` exposes the bearer to the renderer. Set `OPENHUMAN_CORE_REUSE_EXISTING=1` to attach to an externally-started `openhuman-core` process for debugging.

**Where logic lives**

Expand Down Expand Up @@ -301,7 +301,7 @@ Bundled prompts live under **`src/openhuman/agent/prompts/`** at the **repositor

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 per-provider scanners (`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 in-process JSON-RPC server and authenticates inbound RPC with a hex bearer (`OPENHUMAN_CORE_TOKEN`). Stale-listener policy (#1130): on conflict the handle probes `GET /`, decides if the listener is an OpenHuman core, then `kill_pid_term` → `kill_pid_force` with PID revalidation guarding against PID reuse. `restart_core_process` / `start_core_process` Tauri commands let the frontend cycle it for updates.
**Core lifecycle**: `core_process::CoreProcessHandle` spawns the in-process JSON-RPC server and authenticates inbound RPC with a hex bearer that the shell hands the embedded server in-memory via `run_server_embedded_with_ready(rpc_token: Some(_))` (no env-var crossing). Stale-listener policy (#1130): on conflict the handle probes `GET /`, decides if the listener is an OpenHuman core, then `kill_pid_term` → `kill_pid_force` with PID revalidation guarding against PID reuse. `restart_core_process` / `start_core_process` Tauri commands let the frontend cycle it for updates.

Registered IPC commands (see [`gitbooks/developing/architecture/tauri-shell.md`](gitbooks/developing/architecture/tauri-shell.md)) include `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.

Expand Down Expand Up @@ -539,7 +539,7 @@ Follow this order so behavior is **specified**, **proven in Rust**, **proven ove

- **macOS deep links**: Often require a built **`.app`** bundle; not only `tauri dev`.
- **`window.__TAURI__`**: Not assumed at module load; use `isTauri()` (from `app/src/services/webviewAccountService.ts`) or wrap `invoke(...)` in `try/catch`.
- **Core is in-process**: `core_rpc` reaches `http://127.0.0.1:<port>/rpc` (default port `7788`) authenticated with `OPENHUMAN_CORE_TOKEN`. `scripts/stage-core-sidecar.mjs` no longer exists; `pnpm core:stage` is a no-op echo (sidecar removed in PR #1061). For standalone debugging: `./target/debug/openhuman-core serve` writes its token to `{workspace}/core.token` (default `~/.openhuman-staging/core.token` under `OPENHUMAN_APP_ENV=staging`); public endpoints `GET /health`, `GET /schema`, `GET /events` need no auth.
- **Core is in-process**: `core_rpc` reaches `http://127.0.0.1:<port>/rpc` (default port `7788`) authenticated with a per-launch hex bearer. The desktop shell hands the bearer to the embedded server in-memory (no `OPENHUMAN_CORE_TOKEN` on the process env); docker / cloud / VPS operators still supply the bearer via `OPENHUMAN_CORE_TOKEN` (env-as-config). `scripts/stage-core-sidecar.mjs` no longer exists; `pnpm core:stage` is a no-op echo (sidecar removed in PR #1061). For standalone debugging: `./target/debug/openhuman-core serve` writes its token to `{workspace}/core.token` (default `~/.openhuman-staging/core.token` under `OPENHUMAN_APP_ENV=staging`); public endpoints `GET /health`, `GET /schema`, `GET /events` need no auth.

---

Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Commands assume the **repo root**; `pnpm dev` delegates to the `app` workspace.

- **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::CoreProcessHandle` in `app/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_rpc` client) to `http://127.0.0.1:<port>/rpc`, authenticated with a per-launch bearer in `OPENHUMAN_CORE_TOKEN`. Set `OPENHUMAN_CORE_REUSE_EXISTING=1` to attach to an externally-started `openhuman-core` process (e.g. a debug harness).
- **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::CoreProcessHandle` in `app/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_rpc` client) to `http://127.0.0.1:<port>/rpc`, authenticated with a per-launch bearer the shell hands the embedded server in-memory via `run_server_embedded_with_ready(rpc_token: Some(_))`. The renderer reads the same bearer via the `core_rpc_token` Tauri command. `OPENHUMAN_CORE_TOKEN` is still honoured for CLI / docker / cloud env-as-config (operator-supplied) but is no longer set on the process env by the desktop shell. Set `OPENHUMAN_CORE_REUSE_EXISTING=1` to attach to an externally-started `openhuman-core` process (e.g. a debug harness).

**Where logic lives**
- **Rust core**: business logic, execution, domains, RPC, persistence, CLI. Authoritative.
Expand Down Expand Up @@ -197,7 +197,7 @@ No `UserProvider` / `AIProvider` / `SkillProvider` — auth and core snapshot li

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.
**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. The bearer is generated in `CoreProcessHandle::new()` and handed to the embedded server in-memory through `run_server_embedded_with_ready(rpc_token: Some(_))` — never set on the process env. 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`](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).

Expand Down Expand Up @@ -356,4 +356,4 @@ Specify → prove in Rust → prove over RPC → surface in the UI → test.
- **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`). `pnpm dev:app` and all `cargo tauri` scripts call `pnpm tauri:ensure` which runs [`scripts/ensure-tauri-cli.sh`](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`.
- **Tauri environment guard**: use `isTauri()` (from `app/src/services/webviewAccountService.ts`) or wrap `invoke(...)` in `try/catch`; do not check `window.__TAURI__` directly — it is not present at module load and bypasses the established wrapper contract.
- **Core is in-process** (no sidecar): `core_rpc` reaches the embedded server at `http://127.0.0.1:<port>/rpc` with bearer auth via `OPENHUMAN_CORE_TOKEN`. `scripts/stage-core-sidecar.mjs` no longer exists; `pnpm core:stage` is 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.token` under `OPENHUMAN_APP_ENV=staging`).
- **Core is in-process** (no sidecar): `core_rpc` reaches the embedded server at `http://127.0.0.1:<port>/rpc` with bearer auth. The Tauri shell hands the bearer to the embedded server in-memory (no `OPENHUMAN_CORE_TOKEN` on the process env). `scripts/stage-core-sidecar.mjs` no longer exists; `pnpm core:stage` is 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.token` under `OPENHUMAN_APP_ENV=staging`); docker / cloud deployments still supply the bearer via `OPENHUMAN_CORE_TOKEN` in the environment (operator-supplied).
43 changes: 32 additions & 11 deletions app/src-tauri/src/core_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,24 @@ pub struct CoreProcessHandle {
active_port: Arc<RwLock<u16>>,
last_port_fallback: Arc<RwLock<Option<PortFallbackNotice>>>,
/// Bearer token the embedded server validates on every inbound request.
/// Passed to the embedded server through the `OPENHUMAN_CORE_TOKEN`
/// process env var (set in `ensure_running` before spawn) and exposed to
/// the frontend via the `core_rpc_token` Tauri command so every RPC call
/// can include `Authorization: Bearer`.
///
/// Handed to the embedded server **in-memory** (via the `rpc_token`
/// argument of [`openhuman_core::core::jsonrpc::run_server_embedded_with_ready`])
/// rather than through `OPENHUMAN_CORE_TOKEN` on the process environment.
/// Avoiding the env crossing keeps the bearer off `/proc/<pid>/environ`
/// (Linux) and out of `sysctl KERN_PROCARGS2` / `ps eww -p <pid>` (macOS)
/// where any same-UID process could otherwise read it without entitlement.
/// The same value is exposed to the renderer via the `core_rpc_token`
/// Tauri command so every RPC call can attach `Authorization: Bearer`.
rpc_token: Arc<String>,
}

impl CoreProcessHandle {
pub fn new(port: u16) -> Self {
// CURRENT_RPC_TOKEN is intentionally NOT set here. It is published by
// ensure_running() only after the embedded server has been spawned
// with OPENHUMAN_CORE_TOKEN in scope. Setting it here would advertise
// with this token handed over via the in-memory `rpc_token` arg of
// `run_server_embedded_with_ready`. Setting it here would advertise
// a token that an existing process listening on the port (the
// harness-attach fast-path) has never seen, causing 401s on every
// authenticated call.
Expand Down Expand Up @@ -214,11 +220,18 @@ impl CoreProcessHandle {
let mut guard = self.task.lock().await;
if guard.is_none() {
let port = self.preferred_port;
// Set OPENHUMAN_CORE_TOKEN as a process-global env var before
// spawning the embedded server. Same-process tokio task reads
// the same env, matching what a child sidecar would have
// received via Command::env.
std::env::set_var("OPENHUMAN_CORE_TOKEN", self.rpc_token.as_str());
// RPC bearer is handed to the embedded server in-memory
// via the `rpc_token` argument of
// run_server_embedded_with_ready (see below) — never
// through OPENHUMAN_CORE_TOKEN on the process env.
// Sidecar-era env-var transport was a leftover from the
// PR #1061 cleanup; with the core in-process there is no
// child process that needs the env crossing, and
// sharing the bearer via env put it within reach of any
// same-UID process that could read /proc/<pid>/environ
// (Linux) or sysctl KERN_PROCARGS2 / ps eww -p <pid>
// (macOS).
let token_for_core = self.rpc_token.clone();
// Surface the Tauri shell version to the in-process core so
// backend-bound HTTP requests can attach `x-tauri-version`
// analytics headers alongside `x-core-version`.
Expand Down Expand Up @@ -273,12 +286,20 @@ impl CoreProcessHandle {
true,
shutdown_token,
ready_tx,
// In-memory bearer handoff: the embedded server
// seeds its auth subsystem from this value via
// `auth::init_rpc_token_with_value`, so the token
// never crosses OPENHUMAN_CORE_TOKEN on the
// process env.
Some(token_for_core),
)
.await
});
*guard = Some(task);
// Publish only after the embedded server has been spawned
// with OPENHUMAN_CORE_TOKEN in scope.
// with the in-memory bearer in scope. Setting this earlier
// would advertise a token to the frontend that the server
// hadn't loaded yet.
*CURRENT_RPC_TOKEN.write() = Some(self.rpc_token.to_string());
log::debug!("[auth] CURRENT_RPC_TOKEN set after embedded spawn");
}
Expand Down
54 changes: 54 additions & 0 deletions app/src-tauri/src/core_process_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,60 @@ fn ready_signal_updates_runtime_port_and_fallback_notice() {
);
}

/// Regression: `ensure_running` must NOT publish the per-launch RPC bearer
/// to the `OPENHUMAN_CORE_TOKEN` environment variable.
///
/// The bearer is now handed to the in-process core in-memory via the
/// `rpc_token` argument of `run_server_embedded_with_ready`; setting it on
/// the process env would put it within reach of any same-UID process
/// reading `/proc/<pid>/environ` (Linux) or `sysctl KERN_PROCARGS2` /
/// `ps eww -p <pid>` (macOS).
#[test]
fn ensure_running_does_not_publish_token_to_env() {
let _env_lock = env_lock();
let _unset = EnvGuard::unset("OPENHUMAN_CORE_REUSE_EXISTING");
// Force a clean slate so we can assert on the post-spawn value.
let _wipe = EnvGuard::unset("OPENHUMAN_CORE_TOKEN");
let rt = tokio::runtime::Runtime::new().expect("runtime");
let (result, env_after, expected_token, env_during_spawn) = rt.block_on(async {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test listener");
let port = listener.local_addr().expect("local addr").port();
drop(listener);
// Brief yield to let the OS fully release the port.
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

let handle = CoreProcessHandle::new(port);
let expected_token = handle.rpc_token().to_string();
let result = handle.ensure_running().await;
// Capture env immediately after spawn returns Ok — before any
// tokio task could plausibly have set the var.
let env_after = std::env::var("OPENHUMAN_CORE_TOKEN").ok();
// Also peek midway via spawning a tiny check task in the same
// runtime — guards against the codepath setting+removing the var
// within the spawn window.
let env_during_spawn = std::env::var("OPENHUMAN_CORE_TOKEN").ok();
handle.shutdown().await;
(result, env_after, expected_token, env_during_spawn)
});

assert!(
result.is_ok(),
"ensure_running should succeed against a freed port: {result:?}"
);
assert!(
env_after.is_none(),
"ensure_running must NOT publish OPENHUMAN_CORE_TOKEN to the process env \
(sidecar-era leak channel removed). Found: {env_after:?} (handle token was {expected_token:?})"
);
assert!(
env_during_spawn.is_none(),
"OPENHUMAN_CORE_TOKEN must remain unset even momentarily during spawn. \
Found: {env_during_spawn:?}"
);
}

/// Issue #1613: when the preferred port is occupied by a non-OpenHuman
/// listener, startup should fall back to a nearby port instead of failing.
#[test]
Expand Down
Loading
Loading