mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(core): pass in-process RPC bearer via internal handle, not process env (#2709)
This commit is contained in:
+3
-1
@@ -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.
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
@@ -357,4 +357,4 @@ Specify → prove in Rust → prove over RPC → surface in the UI → test.
|
||||
- **macOS deep links**: often require a built `.app` bundle, not just `tauri dev`.
|
||||
- **Windows deep links**: `openhuman://` is registered to `HKCU\Software\Classes\openhuman\shell\open\command` by `tauri-plugin-deep-link::register_all` at first launch (per-user, no UAC). The Tauri shell now reads that key back after `register_all` returns and emits `log::error!` with the actual state (`NotRegistered` / `MissingCommand` / `Stale` / `ReadError`) when the value is missing or doesn't point at the running exe — without it, OAuth callbacks via `openhuman://auth?…` never reach the app (issue #2699). The check lives in [`app/src-tauri/src/deep_link_registration_check.rs`](app/src-tauri/src/deep_link_registration_check.rs); a manual repair script for affected users is in [`gitbooks/overview/troubleshooting-sign-in.md`](gitbooks/overview/troubleshooting-sign-in.md).
|
||||
- **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).
|
||||
|
||||
@@ -69,10 +69,15 @@ 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>,
|
||||
}
|
||||
|
||||
@@ -80,7 +85,8 @@ 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.
|
||||
@@ -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`.
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -64,7 +64,9 @@ request_native_global(method, req) → call and wait for response
|
||||
|
||||
- HTTP JSON-RPC protected by **HTTP Basic Auth**
|
||||
- Realm: `"OpenHuman Hosted Directory"`
|
||||
- Per-launch bearer token stored in `OPENHUMAN_CORE_TOKEN` env var
|
||||
- Per-launch bearer token, transported differently per deployment shape:
|
||||
- **Desktop / Tauri shell**: bearer is generated in `CoreProcessHandle::new()` and held in-memory as `CoreProcessHandle.rpc_token: Arc<String>`, then handed to the embedded server via an internal in-memory handle (`run_server_embedded_with_ready(rpc_token: Some(_))`). **Not** published to the process environment.
|
||||
- **Standalone CLI / Docker / cloud**: bearer is read from the `OPENHUMAN_CORE_TOKEN` env var (via `init_rpc_token`) or from the `{workspace}/core.token` file. This is the operator-supplied configuration surface for those deployments and is intentional.
|
||||
- Frontend obtains bearer via `invoke('core_rpc_token')` Tauri command
|
||||
|
||||
### Stored Credentials
|
||||
@@ -164,7 +166,7 @@ Frontend settings → core RPC (JSON-RPC over HTTP + Basic Auth)
|
||||
3. **Config directory permissions** — `~/.config/openhuman/` and `mcp.json` permission model not reviewed
|
||||
4. **Credential encryption** — `encryption` domain used for at-rest encryption; key management model unclear
|
||||
5. **WebView CSP** — embedded webviews (Telegram, Discord, etc.) loaded under CEF — need to verify CSP headers and iframe restrictions
|
||||
6. **`OPENHUMAN_CORE_TOKEN` in process env** — bearer token in env var; visible via `/proc/self/environ` on Linux or process inspection on macOS
|
||||
6. ~~**`OPENHUMAN_CORE_TOKEN` in process env** — bearer token in env var; visible via `/proc/self/environ` on Linux or process inspection on macOS~~ — **resolved**: in-process core now receives the bearer via an in-memory handoff (`run_server_embedded_with_ready(rpc_token: Some(_))`); the env-var crossing has been removed from the Tauri shell's spawn path. CLI / docker / cloud env-as-config remains the intended operator surface for those deployments.
|
||||
7. **No rate limiting observed** on HTTP JSON-RPC endpoint
|
||||
|
||||
### Positive Signals
|
||||
@@ -183,7 +185,7 @@ Frontend settings → core RPC (JSON-RPC over HTTP + Basic Auth)
|
||||
- [ ] Check file permissions on `~/.config/openhuman/`
|
||||
- [ ] Add rate limiting to HTTP JSON-RPC endpoint
|
||||
- [ ] Document MCP tool output handling expectations
|
||||
- [ ] Review `OPENHUMAN_CORE_TOKEN` lifetime and exposure scope
|
||||
- [x] Review `OPENHUMAN_CORE_TOKEN` lifetime and exposure scope — in-process core now uses in-memory handoff (no env crossing); CLI/docker/cloud retain env-as-config
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,38 +15,74 @@ CORE_PORT="${OPENHUMAN_CORE_PORT:-7788}"
|
||||
CORE_RPC_URL="${CORE_RPC_URL:-http://${CORE_HOST}:${CORE_PORT}/rpc}"
|
||||
|
||||
# Resolve the core RPC bearer token. Resolution order:
|
||||
# 1. OPENHUMAN_CORE_TOKEN env var (set by caller or Tauri shell)
|
||||
# 2. core.token file in workspace dir (written by standalone `openhuman core run`)
|
||||
# 3. Live process environment of the running openhuman-core child (Tauri-managed:
|
||||
# token is injected via env var, never written to disk)
|
||||
# 1. OPENHUMAN_CORE_TOKEN env var (set by caller or via .env / docker / cloud).
|
||||
# Always wins — explicit operator configuration.
|
||||
# 2. Whichever of the two on-disk token files is FRESHEST (newest mtime):
|
||||
# a. config-style core.token file in workspace dir (written by
|
||||
# standalone `openhuman core run`)
|
||||
# b. Debug-only e2e token file written by the Tauri shell at
|
||||
# ${TMPDIR:-/tmp}/openhuman-e2e-rpc-token (mode 0600). Only present
|
||||
# in debug builds — release builds do not write it. The Tauri shell
|
||||
# hands its bearer to the in-process core in-memory; no env-var or
|
||||
# ps-readable surface exists.
|
||||
#
|
||||
# Freshest-wins matters because both files commonly coexist on dev
|
||||
# machines (a previous standalone run leaves $workspace/core.token
|
||||
# behind; the live debug Tauri app writes a new e2e token on each
|
||||
# launch). Static "workspace first" precedence would let a stale
|
||||
# standalone token shadow the live debug bearer.
|
||||
_file_mtime() {
|
||||
# Print the modification time (epoch seconds) of $1 to stdout.
|
||||
# Falls back to 0 if stat is missing or the file does not exist.
|
||||
local f="$1"
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
# macOS (BSD stat) uses -f %m; Linux (GNU stat) uses -c %Y.
|
||||
if stat -f %m "$f" >/dev/null 2>&1; then
|
||||
stat -f %m "$f"
|
||||
elif stat -c %Y "$f" >/dev/null 2>&1; then
|
||||
stat -c %Y "$f"
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
_resolve_rpc_token() {
|
||||
if [[ -n "${OPENHUMAN_CORE_TOKEN:-}" ]]; then
|
||||
echo "core-token source: OPENHUMAN_CORE_TOKEN env var" >&2
|
||||
echo "$OPENHUMAN_CORE_TOKEN"
|
||||
return
|
||||
fi
|
||||
|
||||
local workspace="${OPENHUMAN_WORKSPACE:-$HOME/.openhuman}"
|
||||
local token_file="$workspace/core.token"
|
||||
if [[ -f "$token_file" ]]; then
|
||||
cat "$token_file"
|
||||
return
|
||||
local workspace_token_file="$workspace/core.token"
|
||||
local e2e_token_file="${TMPDIR:-/tmp}/openhuman-e2e-rpc-token"
|
||||
e2e_token_file="${e2e_token_file%/}"
|
||||
|
||||
local workspace_mtime
|
||||
local e2e_mtime
|
||||
workspace_mtime="$(_file_mtime "$workspace_token_file")"
|
||||
e2e_mtime="$(_file_mtime "$e2e_token_file")"
|
||||
|
||||
if [[ "$workspace_mtime" -eq 0 && "$e2e_mtime" -eq 0 ]]; then
|
||||
echo "ERROR: core RPC token not found. Options:" >&2
|
||||
echo " 1. Set OPENHUMAN_CORE_TOKEN=<token> before running this script" >&2
|
||||
echo " 2. Start the core standalone: openhuman core run (writes $workspace_token_file)" >&2
|
||||
echo " 3. Run the OpenHuman app in debug mode (writes $e2e_token_file)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Tauri-managed core: token is in the child process environment, not on disk.
|
||||
# Read it from the running openhuman-core process via ps.
|
||||
local core_pid
|
||||
core_pid="$(pgrep -f 'openhuman-core.*run' 2>/dev/null | head -1)"
|
||||
if [[ -n "$core_pid" ]]; then
|
||||
local tok
|
||||
tok="$(ps eww -p "$core_pid" 2>/dev/null | tr ' ' '\n' | grep '^OPENHUMAN_CORE_TOKEN=' | cut -d= -f2)"
|
||||
if [[ -n "$tok" ]]; then
|
||||
echo "$tok"
|
||||
return
|
||||
fi
|
||||
|
||||
# Pick whichever exists. If both exist, prefer the freshest by mtime —
|
||||
# that's the live debug bearer, not the stale standalone leftover.
|
||||
if [[ "$e2e_mtime" -gt "$workspace_mtime" ]]; then
|
||||
echo "core-token source: debug e2e file ($e2e_token_file, mtime=$e2e_mtime)" >&2
|
||||
cat "$e2e_token_file"
|
||||
else
|
||||
echo "core-token source: workspace file ($workspace_token_file, mtime=$workspace_mtime)" >&2
|
||||
cat "$workspace_token_file"
|
||||
fi
|
||||
echo "ERROR: core RPC token not found. Options:" >&2
|
||||
echo " 1. Set OPENHUMAN_CORE_TOKEN=<token> before running this script" >&2
|
||||
echo " 2. Start the core standalone: openhuman core run (writes $token_file)" >&2
|
||||
echo " 3. Open the OpenHuman app (token auto-detected from process env)" >&2
|
||||
exit 1
|
||||
}
|
||||
RPC_TOKEN="$(_resolve_rpc_token)"
|
||||
KEEP_TUNNEL=0
|
||||
|
||||
+135
-29
@@ -1,20 +1,31 @@
|
||||
//! Per-process RPC bearer-token authentication.
|
||||
//!
|
||||
//! At server startup, [`init_rpc_token`] either reads the token from the
|
||||
//! `OPENHUMAN_CORE_TOKEN` environment variable (Tauri-spawned path) or
|
||||
//! generates a 256-bit cryptographically-random token and writes it to
|
||||
//! `{workspace_dir}/core.token` (owner-read-only on Unix, standalone CLI path),
|
||||
//! then stores it in a process-global [`OnceLock`].
|
||||
//! Three initialization paths feed the process-global [`OnceLock`] that holds
|
||||
//! the active bearer token:
|
||||
//!
|
||||
//! **Tauri path**: the Tauri shell generates the token in
|
||||
//! `CoreProcessHandle::new()`, injects it as `OPENHUMAN_CORE_TOKEN` before
|
||||
//! spawning the core process, and holds it in memory via
|
||||
//! `CoreProcessHandle.rpc_token`. The shell includes the token in every
|
||||
//! request as `Authorization: Bearer <token>`. The `core.token` file is
|
||||
//! never written in this path.
|
||||
//! 1. **In-memory handoff (preferred for the in-process core)** —
|
||||
//! [`init_rpc_token_with_value`] sets the token directly from a value the
|
||||
//! Tauri shell already holds in `CoreProcessHandle.rpc_token`. No env var
|
||||
//! is read or set; the token never crosses a process-global env surface.
|
||||
//! This is the path the Tauri host uses now that the core runs in-process
|
||||
//! (PR #1061) — same-process handoff makes the env crossing unnecessary,
|
||||
//! and avoiding it keeps the token off `/proc/<pid>/environ` (Linux) and
|
||||
//! out of `sysctl KERN_PROCARGS2` / `ps eww -p <pid>` (macOS) where any
|
||||
//! same-UID process could read it without entitlement.
|
||||
//! 2. **Env-as-config fallback** — when no in-memory token is supplied,
|
||||
//! [`init_rpc_token`] reads `OPENHUMAN_CORE_TOKEN` from the environment.
|
||||
//! This is the legitimate operator-supplied transport for Docker / cloud /
|
||||
//! VPS deployments where the bearer must come from `fly secrets set …`,
|
||||
//! `docker run -e …`, or a systemd unit file — there is no live shell
|
||||
//! handing it to the binary in-memory.
|
||||
//! 3. **Standalone CLI fallback** — when neither path supplies a token, the
|
||||
//! core generates a fresh 256-bit token and writes it to
|
||||
//! `{workspace_dir}/core.token` (owner-read-only on Unix) so external CLI
|
||||
//! clients can authenticate.
|
||||
//!
|
||||
//! **Standalone CLI path**: the core generates a fresh token and writes it to
|
||||
//! `{workspace_dir}/core.token` so that CLI clients can read and use it.
|
||||
//! Once set, the in-memory `OnceLock` is the single source of truth — all
|
||||
//! transports ([`rpc_auth_middleware`], Socket.IO, SSE query-token fallback,
|
||||
//! the approval-gate session id) read via [`get_rpc_token`].
|
||||
//!
|
||||
//! Endpoints exempt from auth (checked by [`rpc_auth_middleware`]):
|
||||
//! - `GET /` — public info page
|
||||
@@ -89,29 +100,51 @@ const PUBLIC_PATHS: &[&str] = &[
|
||||
/// (#1339) is the next planned addition.
|
||||
const QUERY_TOKEN_PATHS: &[&str] = &["/events/webhooks"];
|
||||
|
||||
/// The environment variable the Tauri shell sets before spawning the core.
|
||||
/// Operator-supplied environment variable that carries the RPC bearer in
|
||||
/// non-desktop deployments.
|
||||
///
|
||||
/// When this variable is present the core uses its value as the RPC token
|
||||
/// (no file I/O needed). When absent (standalone `openhuman core run`) the
|
||||
/// core generates a token and writes it to `{workspace_dir}/core.token` so
|
||||
/// **The Tauri desktop shell does NOT set this variable.** Since PR #1061
|
||||
/// the core runs in-process inside the Tauri host, and the shell hands the
|
||||
/// per-launch bearer to the embedded server via an internal in-memory handle
|
||||
/// (see [`init_rpc_token_with_value`]). The desktop boot flow never crosses
|
||||
/// a process-global env surface.
|
||||
///
|
||||
/// `OPENHUMAN_CORE_TOKEN` remains the canonical configuration surface for
|
||||
/// **standalone CLI / Docker / cloud** deployments only — where the bearer
|
||||
/// must come from `fly secrets set …`, `docker run -e …`, a systemd unit
|
||||
/// file, or a developer running `openhuman-core serve` from a shell with the
|
||||
/// env var pre-set. In those shapes there is no live host process to hand
|
||||
/// the token over in-memory, so env-as-config is the appropriate transport.
|
||||
///
|
||||
/// When this variable is present [`init_rpc_token`] uses its value (no file
|
||||
/// I/O). When absent and no in-memory token was seeded, `init_rpc_token`
|
||||
/// generates a fresh token and writes it to `{workspace_dir}/core.token` so
|
||||
/// CLI clients can authenticate.
|
||||
pub const CORE_TOKEN_ENV_VAR: &str = "OPENHUMAN_CORE_TOKEN";
|
||||
|
||||
/// Initialize the per-process RPC token.
|
||||
/// Initialize the per-process RPC token from env-or-file (non-desktop path).
|
||||
///
|
||||
/// **Preferred path — Tauri-spawned core**: reads the token from the
|
||||
/// `OPENHUMAN_CORE_TOKEN` environment variable set by the Tauri shell. No
|
||||
/// file is written; the token is always available the instant the process
|
||||
/// starts.
|
||||
/// **Not the desktop path.** The Tauri shell passes the per-launch bearer
|
||||
/// to the embedded server via the internal in-memory handle (see
|
||||
/// [`init_rpc_token_with_value`]); it does **not** set
|
||||
/// `OPENHUMAN_CORE_TOKEN`. This function is the bootstrap path for
|
||||
/// standalone CLI / Docker / cloud deployments.
|
||||
///
|
||||
/// **Fallback — standalone CLI**: generates a fresh 256-bit token, writes it
|
||||
/// to `{workspace_dir}/core.token` (owner-read-only on Unix) for external
|
||||
/// callers, and stores it in the process global.
|
||||
/// **Env-as-config (preferred for non-desktop)**: when
|
||||
/// `OPENHUMAN_CORE_TOKEN` is set in the process environment (typically by
|
||||
/// the container runtime, secrets manager, or systemd unit file), the core
|
||||
/// uses its value as the RPC token. No file is written; the token is
|
||||
/// available the instant the process starts.
|
||||
///
|
||||
/// **Standalone CLI fallback**: when no env var is supplied, the core
|
||||
/// generates a fresh 256-bit token, writes it to `{workspace_dir}/core.token`
|
||||
/// (owner-read-only on Unix) for external callers, and stores it in the
|
||||
/// process global.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error only in the fallback path, if the token file cannot be
|
||||
/// written.
|
||||
/// Returns an error only in the standalone fallback path, if the token file
|
||||
/// cannot be written.
|
||||
pub fn init_rpc_token(workspace_dir: &Path) -> anyhow::Result<()> {
|
||||
// Idempotency guard: if the token is already set, do nothing. A second
|
||||
// call must never write a new token to disk while the process still
|
||||
@@ -122,12 +155,16 @@ pub fn init_rpc_token(workspace_dir: &Path) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Fast path: token pre-seeded by the Tauri shell via env var.
|
||||
// Env-as-config path: bearer supplied by the operator via
|
||||
// OPENHUMAN_CORE_TOKEN. Used by Docker / cloud / systemd / a developer
|
||||
// running `openhuman-core serve` from a pre-configured shell. Desktop
|
||||
// (Tauri) does NOT set this variable — it uses `init_rpc_token_with_value`
|
||||
// for an in-memory handoff instead.
|
||||
if let Ok(env_token) = std::env::var(CORE_TOKEN_ENV_VAR) {
|
||||
let env_token = env_token.trim().to_string();
|
||||
if !env_token.is_empty() {
|
||||
let _ = RPC_TOKEN.set(env_token);
|
||||
log::info!("[auth] core RPC token loaded from environment (Tauri-managed)");
|
||||
log::info!("[auth] core RPC token loaded from environment (operator-supplied)");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -144,6 +181,38 @@ pub fn init_rpc_token(workspace_dir: &Path) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Seed the per-process RPC token directly from a caller-supplied value.
|
||||
///
|
||||
/// **In-memory handoff path** — used by the Tauri shell to inject the bearer
|
||||
/// the host generated in `CoreProcessHandle::new()` into the in-process core
|
||||
/// without round-tripping through `OPENHUMAN_CORE_TOKEN` in the process
|
||||
/// environment. The token never lands on a process-global env surface, which
|
||||
/// keeps it 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.
|
||||
///
|
||||
/// Idempotent: a second call is a no-op (matches [`init_rpc_token`] — flipping
|
||||
/// the in-memory bearer mid-life would 401 every in-flight client).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error only if `token` is empty after trimming. A non-empty
|
||||
/// token is accepted as-is — callers are expected to have generated a
|
||||
/// CSPRNG hex string (see `CoreProcessHandle::generate_rpc_token`).
|
||||
pub fn init_rpc_token_with_value(token: &str) -> anyhow::Result<()> {
|
||||
let trimmed = token.trim();
|
||||
if trimmed.is_empty() {
|
||||
anyhow::bail!("init_rpc_token_with_value: supplied token is empty");
|
||||
}
|
||||
if RPC_TOKEN.get().is_some() {
|
||||
log::debug!("[auth] init_rpc_token_with_value: already initialized, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
let _ = RPC_TOKEN.set(trimmed.to_string());
|
||||
log::info!("[auth] core RPC token loaded via in-memory handoff (no env crossing)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the active RPC token, if initialized.
|
||||
pub fn get_rpc_token() -> Option<&'static str> {
|
||||
RPC_TOKEN.get().map(String::as_str)
|
||||
@@ -415,6 +484,43 @@ mod tests {
|
||||
assert!(!verify_bearer_token(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_rpc_token_with_value_rejects_empty() {
|
||||
// Trimmed-empty values must error rather than seed an empty bearer.
|
||||
assert!(init_rpc_token_with_value("").is_err());
|
||||
assert!(init_rpc_token_with_value(" ").is_err());
|
||||
}
|
||||
|
||||
/// `init_rpc_token_with_value` populates the same `RPC_TOKEN` OnceLock
|
||||
/// that `get_rpc_token` reads — i.e. the in-memory handoff path produces
|
||||
/// the bearer everyone else (HTTP middleware, Socket.IO verifier,
|
||||
/// approval-gate session_id) reads from. We can't deterministically
|
||||
/// assert the *value* set here (the OnceLock may already be seeded by a
|
||||
/// sibling test that ran first in the same binary), but we can assert
|
||||
/// the OnceLock is initialised after this call returns Ok, and that the
|
||||
/// helper is idempotent.
|
||||
#[test]
|
||||
fn init_rpc_token_with_value_seeds_and_is_idempotent() {
|
||||
// First call: either we seed, or a sibling test already did. Either
|
||||
// way the helper must return Ok and leave `get_rpc_token` populated.
|
||||
let token = "cafebabe1234567890abcdef0123456789abcdef0123456789abcdef01234567";
|
||||
init_rpc_token_with_value(token).expect("seed succeeds");
|
||||
assert!(
|
||||
get_rpc_token().is_some(),
|
||||
"after init_rpc_token_with_value, get_rpc_token must return Some"
|
||||
);
|
||||
// Second call is a no-op (matching init_rpc_token semantics) — must
|
||||
// not error, must not flip the in-memory value.
|
||||
let before = get_rpc_token().map(str::to_string);
|
||||
init_rpc_token_with_value("a-different-value-that-must-be-ignored")
|
||||
.expect("idempotent re-init succeeds");
|
||||
let after = get_rpc_token().map(str::to_string);
|
||||
assert_eq!(
|
||||
before, after,
|
||||
"second init_rpc_token_with_value must not flip the in-memory bearer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_query_token_returns_none_on_missing_query() {
|
||||
assert_eq!(extract_query_token(None), None);
|
||||
|
||||
+69
-15
@@ -1261,7 +1261,7 @@ pub async fn run_server(
|
||||
port: Option<u16>,
|
||||
socketio_enabled: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
run_server_inner(host, port, socketio_enabled, false, None, None).await
|
||||
run_server_inner(host, port, socketio_enabled, false, None, None, None).await
|
||||
}
|
||||
|
||||
/// Like [`run_server`] but marks the instance as embedded.
|
||||
@@ -1278,17 +1278,26 @@ pub async fn run_server_embedded(
|
||||
true,
|
||||
Some(shutdown_token),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Embedded entrypoint with an explicit readiness callback.
|
||||
///
|
||||
/// When the caller already holds the per-launch RPC bearer in memory (the
|
||||
/// Tauri shell now that the core runs in-process — PR #1061), it should
|
||||
/// pass `Some(token)` so the embedded server can seed its auth subsystem
|
||||
/// via [`crate::core::auth::init_rpc_token_with_value`] without ever
|
||||
/// reading `OPENHUMAN_CORE_TOKEN` from the process environment. Passing
|
||||
/// `None` preserves the env-as-config fallback (CLI / docker / cloud).
|
||||
pub async fn run_server_embedded_with_ready(
|
||||
host: Option<&str>,
|
||||
port: Option<u16>,
|
||||
socketio_enabled: bool,
|
||||
shutdown_token: CancellationToken,
|
||||
ready_tx: tokio::sync::oneshot::Sender<EmbeddedReadySignal>,
|
||||
rpc_token: Option<std::sync::Arc<String>>,
|
||||
) -> anyhow::Result<()> {
|
||||
run_server_inner(
|
||||
host,
|
||||
@@ -1297,6 +1306,7 @@ pub async fn run_server_embedded_with_ready(
|
||||
true,
|
||||
Some(shutdown_token),
|
||||
Some(ready_tx),
|
||||
rpc_token,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1309,6 +1319,7 @@ async fn run_server_inner(
|
||||
embedded_core: bool,
|
||||
shutdown_token: Option<CancellationToken>,
|
||||
ready_tx: Option<tokio::sync::oneshot::Sender<EmbeddedReadySignal>>,
|
||||
rpc_token: Option<std::sync::Arc<String>>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Ensure all controllers are registered before starting.
|
||||
let _ = all::all_registered_controllers();
|
||||
@@ -1319,13 +1330,30 @@ async fn run_server_inner(
|
||||
crate::openhuman::keyring::init_master_key();
|
||||
|
||||
// Initialize the per-process RPC bearer token.
|
||||
// Written to {workspace_dir}/core.token so the Tauri shell can read it.
|
||||
let token_dir = crate::openhuman::config::default_root_openhuman_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".openhuman")
|
||||
});
|
||||
crate::core::auth::init_rpc_token(&token_dir)?;
|
||||
//
|
||||
// Preferred path (in-process core spawned by the Tauri shell): the caller
|
||||
// passes the bearer it already holds in `CoreProcessHandle.rpc_token` as
|
||||
// `rpc_token: Some(_)`. The token is seeded directly into the auth
|
||||
// subsystem without ever crossing `OPENHUMAN_CORE_TOKEN` on the process
|
||||
// environment — closing the same-UID readback channel (sysctl
|
||||
// KERN_PROCARGS2 / ps eww on macOS, /proc/<pid>/environ on Linux).
|
||||
//
|
||||
// Fallback (standalone CLI / docker / cloud `openhuman core run`):
|
||||
// `rpc_token: None` lets `init_rpc_token` read `OPENHUMAN_CORE_TOKEN`
|
||||
// from the environment when present (env-as-config — legit operator
|
||||
// surface), or generate a fresh token and write `{workspace_dir}/core.token`
|
||||
// (0o600 on Unix) so CLI callers can authenticate.
|
||||
if let Some(token) = rpc_token.as_deref() {
|
||||
crate::core::auth::init_rpc_token_with_value(token)?;
|
||||
} else {
|
||||
let token_dir =
|
||||
crate::openhuman::config::default_root_openhuman_dir().unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".openhuman")
|
||||
});
|
||||
crate::core::auth::init_rpc_token(&token_dir)?;
|
||||
}
|
||||
|
||||
// Initialize the global MemoryClient so composio providers
|
||||
// (gmail/slack/notion) can persist their sync_state via kv_get/kv_set,
|
||||
@@ -1413,11 +1441,34 @@ async fn run_server_inner(
|
||||
// explicit RPC token. Without this, the entire RPC surface (tool
|
||||
// execution, file access, credentials) is unauthenticated and reachable
|
||||
// from the network. See: https://github.com/tinyhumansai/openhuman/issues/1919
|
||||
//
|
||||
// "Explicit token" means any of:
|
||||
// - An in-memory bearer supplied by the embedded caller via the
|
||||
// `rpc_token` parameter (the Tauri shell hands its
|
||||
// `CoreProcessHandle.rpc_token` in this way — see
|
||||
// `init_rpc_token_with_value`). This never lands on the process env.
|
||||
// - `OPENHUMAN_CORE_TOKEN` set in the process environment (operator
|
||||
// config for standalone CLI / Docker / cloud).
|
||||
//
|
||||
// Checking only the env var would emit a false security warning whenever
|
||||
// an embedded caller binds on a non-loopback host with an in-memory
|
||||
// bearer — the server is already protected in that case.
|
||||
if crate::openhuman::security::pairing::is_public_bind(&resolved_host) {
|
||||
let has_explicit_token = std::env::var(crate::core::auth::CORE_TOKEN_ENV_VAR)
|
||||
let has_in_memory_token = rpc_token
|
||||
.as_deref()
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let has_env_token = std::env::var(crate::core::auth::CORE_TOKEN_ENV_VAR)
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.is_some();
|
||||
// Auth subsystem was already initialised above; fall back to the live
|
||||
// token if neither input matched but somehow a token is seeded (e.g.
|
||||
// a future caller route that doesn't thread the value through here).
|
||||
let has_initialized_token = crate::core::auth::get_rpc_token()
|
||||
.map(|t| !t.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
let has_explicit_token = has_in_memory_token || has_env_token || has_initialized_token;
|
||||
if !has_explicit_token {
|
||||
log::error!(
|
||||
"[core] ⚠️ SECURITY WARNING: Binding on public address {resolved_host} without \
|
||||
@@ -1864,16 +1915,19 @@ pub async fn bootstrap_core_runtime(embedded_core: bool) {
|
||||
})
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let (session_id, ephemeral) = match std::env::var("OPENHUMAN_CORE_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
Some(token) => (token, false),
|
||||
// Read the active bearer from the in-memory auth subsystem instead of
|
||||
// OPENHUMAN_CORE_TOKEN — same value (init_rpc_token / init_rpc_token_with_value
|
||||
// both populate the OnceLock that get_rpc_token reads), no env crossing.
|
||||
// Init ordering: run_server_inner seeds the auth subsystem above
|
||||
// (line ~1340) before bootstrap_core_runtime runs, so the bearer is
|
||||
// always present here when supplied.
|
||||
let (session_id, ephemeral) = match crate::core::auth::get_rpc_token() {
|
||||
Some(token) => (token.to_string(), false),
|
||||
None => (format!("session-{}", uuid::Uuid::new_v4()), true),
|
||||
};
|
||||
if ephemeral {
|
||||
log::debug!(
|
||||
"[runtime] OPENHUMAN_CORE_TOKEN unset; generated ephemeral session_id={session_id} \
|
||||
"[runtime] auth bearer uninitialized; generated ephemeral session_id={session_id} \
|
||||
for approval gate — `approval_list_pending` is session-agnostic so pending rows \
|
||||
from prior launches will still be visible, but per-session audit grouping will not \
|
||||
correlate across restarts"
|
||||
|
||||
@@ -22,7 +22,13 @@ const TEST_RPC_TOKEN: &str = "inference-http-tests-token";
|
||||
/// Initialize the per-process RPC bearer token exactly once, so that the
|
||||
/// auth middleware can answer 401 instead of 500 ("auth subsystem not
|
||||
/// initialized") in tests that don't spin up a real core.
|
||||
fn ensure_test_rpc_auth() {
|
||||
///
|
||||
/// Returns the bearer that is actually installed in `RPC_TOKEN` after init.
|
||||
/// This may not equal `TEST_RPC_TOKEN`: the `RPC_TOKEN` `OnceLock` is shared
|
||||
/// across the whole test binary, and a sibling test (e.g. the in-memory-seed
|
||||
/// regression in `src/core/auth.rs`) may have populated it first. Token-
|
||||
/// agnostic callers should treat the return value as the source of truth.
|
||||
fn ensure_test_rpc_auth() -> String {
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
// SAFETY: test-only init; we serialize via `Once`, and live_routing_e2e
|
||||
@@ -32,11 +38,14 @@ fn ensure_test_rpc_auth() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir for token file");
|
||||
crate::core::auth::init_rpc_token(tmp.path()).expect("init rpc auth token for http tests");
|
||||
});
|
||||
crate::core::auth::get_rpc_token()
|
||||
.expect("rpc bearer must be installed after ensure_test_rpc_auth")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Build the test router (Socket.IO disabled — no real runtime needed).
|
||||
fn test_router() -> axum::Router {
|
||||
ensure_test_rpc_auth();
|
||||
let _ = ensure_test_rpc_auth();
|
||||
build_core_http_router(false)
|
||||
}
|
||||
|
||||
@@ -93,9 +102,10 @@ async fn test_models_no_bearer_returns_401() {
|
||||
/// test only asserts that auth passed.
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_with_bearer_not_rejected_as_auth_error() {
|
||||
// Use the same token that `ensure_test_rpc_auth` installed via the
|
||||
// `Once` initializer in this module.
|
||||
let token = TEST_RPC_TOKEN.to_string();
|
||||
// Read whatever token is actually installed in `RPC_TOKEN`. A sibling
|
||||
// test in `src/core/auth.rs` may seed the `OnceLock` first with its
|
||||
// own value, so we cannot rely on `TEST_RPC_TOKEN` matching.
|
||||
let token = ensure_test_rpc_auth();
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": "ollama:llama3",
|
||||
|
||||
Reference in New Issue
Block a user