Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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