refactor: split tauri host from openhuman_core runtime (#43)

* chore: add Cargo.toml and Cargo.lock for rust-core workspace

- Introduced a new workspace for the rust-core module with its own Cargo.toml.
- Added Cargo.lock to manage dependencies for the rust-core module.
- Updated .gitignore to exclude the target directory generated by Cargo.
- Modified GitHub Actions workflow to build from the new rust-core path instead of src-tauri.

* feat: enhance memory and authentication models in rust-core

- Added new `memory` module to handle persistent memory operations for skills, including methods for storing and querying skill data.
- Introduced `models` module with `auth` and `socket` submodules to manage user session and socket connection states.
- Updated `lib.rs` to include new modules and ensure proper integration within the rust-core workspace.
- Modified `eslint.config.js` to ignore target directories in linting processes.

* feat: integrate Tauri and QuickJS support in rust-core

- Added `tauri` and `rquickjs` as optional dependencies in `Cargo.toml` to enable Tauri integration and JavaScript execution.
- Introduced `CronScheduler` and `PingScheduler` modules for managing scheduled tasks and health checks for skills.
- Implemented `MemoryState` struct for shared app-state management in the memory client.
- Updated the runtime module to include new schedulers and ensure proper integration with Tauri features.
- Enhanced the skill registry to support new functionalities related to skill management and communication.

* chore: update ESLint configuration and refactor Rust module imports

- Added 'rust-core/**' to ESLint ignore list to streamline linting processes.
- Refactored import paths in Rust modules to directly reference the `memory` module, enhancing clarity and maintainability.
- Improved formatting in JavaScript files for better readability and consistency.

* refactor: rename rust-core to openhuman-core and update dependencies

- Renamed the `rust-core` module to `openhuman-core` across all files for consistency.
- Updated `Cargo.lock` to include `openhuman-core` as a new dependency and removed `rust-core`.
- Adjusted import paths in the codebase to reflect the new module name, ensuring all references are updated.
- Enhanced the Tauri integration by modifying dependencies in `src-tauri/Cargo.toml` to point to `openhuman-core`.

* refactor: simplify platform detection using match statements

- Replaced multiple if-else statements with match expressions in `current_platform`, `get_platform`, and `register` functions for improved readability and maintainability.
- Removed unnecessary conditional compilation for Android and iOS in the `SocketManager` and `tauri_bridge` modules, streamlining the codebase.
- Enhanced the `send_notification` function to handle platform checks more efficiently.

* refactor: update platform detection to use std::env::consts

- Replaced `cfg!(target_os = "os_name")` checks with `std::env::consts::OS` for improved clarity and consistency across the codebase.
- Added `rppal` as an optional dependency in `Cargo.toml` for Raspberry Pi support.
- Cleaned up platform-specific code in various modules, enhancing maintainability.

* refactor: streamline platform-specific code and improve readability

- Replaced conditional compilation with `std::env::consts::OS` checks in various modules to enhance clarity and maintainability.
- Simplified platform detection logic in `available_disk_space_mb`, `ensure_arduino_cli`, and `open_in_brave` functions.
- Updated `screenshot_command_exists` test to conditionally skip based on the operating system.
- Cleaned up unnecessary `#[cfg]` attributes, focusing on a more consistent approach across the codebase.

* feat: introduce comprehensive AI configuration and memory management

- Added multiple configuration files for OpenHuman AI, including `AGENTS.md`, `BOOTSTRAP.md`, `CONSCIOUS_LOOP.md`, `IDENTITY.md`, `MEMORY.md`, `README.md`, `SOUL.md`, `TOOLS.md`, and `USER.md` to define agent roles, onboarding processes, identity, memory management, and tool capabilities.
- Implemented an encryption layer for AI memory storage in `encryption.rs`, utilizing AES-256-GCM for secure data handling.
- Updated `lib.rs` to include the new AI module structure, enhancing the overall architecture and maintainability of the codebase.

* refactor: update dependencies and configuration for improved structure

- Removed `android_logger` and related packages from `Cargo.lock` and `Cargo.toml`, streamlining the dependency list.
- Adjusted the `APP_IDENTIFIER` constant in `config.rs` to reflect the new application identifier.
- Updated resource paths in `tauri.conf.json` for better organization.
- Enhanced platform-specific dependency management in `Cargo.toml` for clarity and maintainability.

* fix(core): gate ai module behind tauri-host feature

* refactor: update AI loading mechanisms and improve platform handling

- Refactored the AI configuration loading in `loader.ts` and `tools/loader.ts` to utilize Tauri commands for desktop environments, enhancing performance and reliability.
- Updated paths for AI markdown files to reflect the new `rust-core` structure.
- Introduced a new end-to-end test for Tauri command interactions in `tauriCoreBridge.e2e.test.ts`.
- Cleaned up platform-specific code across various modules, ensuring a more consistent approach to handling desktop and web contexts.

* feat: add sidecar core binary build and staging for Tauri bundler

- Implemented build steps for the sidecar core binary in multiple workflows, targeting both x86_64-unknown-linux-gnu and aarch64-apple-darwin architectures.
- Added staging steps to copy the built binaries into the Tauri resources directory, ensuring proper integration for application bundling.
- Updated relevant workflows to enhance the build process and streamline artifact management.

* refactor: enhance AI loading logic for Tauri integration

- Updated `loader.ts` and `tools/loader.ts` to prioritize Tauri commands for loading configurations in desktop environments, with a fallback to bundled markdown files for web contexts.
- Adjusted test mocks to reflect the new file paths for tools markdown.
- Improved test setup to mock Tauri API behavior accurately.

* docs: update CLAUDE.md and remove Android build scripts from package.json

- Added a new section in CLAUDE.md detailing the runtime scope, clarifying that Tauri is desktop-only and should not include mobile or web branches.
- Removed Android development and build scripts from package.json to streamline the project for desktop platforms only.

* refactor: streamline import statements and enhance test mock structure

- Reordered import statements in `loader.ts` and `tools/loader.ts` for consistency.
- Simplified mock implementation in `tauriCoreBridge.e2e.test.ts` to improve readability and maintainability.
- Added external binary configuration in `build.rs` to support resource management during local builds.

* test: stabilize core/unit test paths and tauri build-test config

* chore: update subproject commit reference in skills

* chore: update Tauri build configuration to include custom environment variable

- Modified the Tauri build command in the GitHub Actions workflow to set a custom configuration for updater artifacts, enhancing the build process for the x86_64-unknown-linux-gnu target.
This commit is contained in:
Steven Enamakel
2026-03-27 13:30:32 -07:00
committed by GitHub
parent f7223f7fe7
commit 37991fc567
306 changed files with 12662 additions and 1863 deletions
+12 -1
View File
@@ -72,7 +72,18 @@ jobs:
- name: Install skills dependencies
run: cd skills && yarn install --frozen-lockfile
- name: Build sidecar core binary
run: cargo build --manifest-path rust-core/Cargo.toml --release --target x86_64-unknown-linux-gnu --bin openhuman-core
- name: Stage sidecar for Tauri bundler
run: |
mkdir -p src-tauri/binaries
cp rust-core/target/x86_64-unknown-linux-gnu/release/openhuman-core src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
chmod +x src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
- name: Build Tauri app
run: yarn tauri build --bundles none -- --target x86_64-unknown-linux-gnu
run: |
TAURI_CONFIG_OVERRIDE='{"bundle":{"createUpdaterArtifacts":false}}'
yarn tauri build -c "$TAURI_CONFIG_OVERRIDE" --bundles none -- --target x86_64-unknown-linux-gnu
env:
NODE_ENV: production
+17
View File
@@ -87,6 +87,23 @@ jobs:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Build sidecar core binary
shell: bash
run: |
cargo build \
--manifest-path rust-core/Cargo.toml \
--release \
--target aarch64-apple-darwin \
--bin openhuman-core
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
mkdir -p src-tauri/binaries
cp rust-core/target/aarch64-apple-darwin/release/openhuman-core \
src-tauri/binaries/openhuman-core-aarch64-apple-darwin
chmod +x src-tauri/binaries/openhuman-core-aarch64-apple-darwin
- name: Build, package (no GitHub release upload)
uses: tauri-apps/tauri-action@v0.6.2
env:
+25 -3
View File
@@ -167,8 +167,8 @@ jobs:
args: '--target x86_64-apple-darwin'
target: 'x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
target: ''
args: '--target x86_64-unknown-linux-gnu'
target: 'x86_64-unknown-linux-gnu'
runs-on: ${{ matrix.settings.platform }}
steps:
- name: Checkout
@@ -288,6 +288,28 @@ jobs:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Build sidecar core binary
shell: bash
run: |
cargo build \
--manifest-path rust-core/Cargo.toml \
--release \
--target "$MATRIX_TARGET" \
--bin openhuman-core
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
mkdir -p src-tauri/binaries
SOURCE="rust-core/target/$MATRIX_TARGET/release/openhuman-core"
DEST="src-tauri/binaries/openhuman-core-$MATRIX_TARGET"
cp "$SOURCE" "$DEST"
chmod +x "$DEST"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Build, package and publish
uses: tauri-apps/tauri-action@v0.6.2
id: build-tauri
@@ -305,7 +327,7 @@ jobs:
# macOS 10.15+ required for std::filesystem used by llama.cpp
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
with:
args: '-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}'
args: '-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }} -- -- --bin OpenHuman'
includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }}
includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }}
# TDLib dylibs are now bundled natively via build.rs + tauri.conf.json macOS.frameworks
+36 -23
View File
@@ -219,8 +219,8 @@ jobs:
# target: x86_64-apple-darwin
# artifact_suffix: x86_64-apple-darwin
- platform: ubuntu-22.04
args: ''
target: ''
args: --target x86_64-unknown-linux-gnu
target: x86_64-unknown-linux-gnu
artifact_suffix: ubuntu
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -305,6 +305,28 @@ jobs:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Build sidecar core binary
shell: bash
run: |
cargo build \
--manifest-path rust-core/Cargo.toml \
--release \
--target "$MATRIX_TARGET" \
--bin openhuman-core
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Stage sidecar for Tauri bundler
shell: bash
run: |
mkdir -p src-tauri/binaries
SOURCE="rust-core/target/$MATRIX_TARGET/release/openhuman-core"
DEST="src-tauri/binaries/openhuman-core-$MATRIX_TARGET"
cp "$SOURCE" "$DEST"
chmod +x "$DEST"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Build, package and upload to release
uses: tauri-apps/tauri-action@v0.6.2
id: tauri-build
@@ -328,51 +350,43 @@ jobs:
# the desktop UI app binary is packaged.
# Yarn v1 strips one "--" layer when invoking scripts, hence the
# double-separator when forwarding flags to Cargo.
args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}
args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }} -- -- --bin OpenHuman
includeDebug: false
includeRelease: true
releaseId: ${{ needs.create-release.outputs.release_id }}
owner: tinyhumansai
repo: openhuman
- name: Verify macOS app bundle excludes standalone core binary
- name: Verify macOS app bundle sidecar layout
if: matrix.settings.platform == 'macos-latest'
shell: bash
run: |
APP_PATH="src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app"
echo "Inspecting bundle at: $APP_PATH"
ls -la "$APP_PATH/Contents/MacOS"
ls -la "$APP_PATH/Contents/Resources" | grep openhuman-core || true
if [ -f "$APP_PATH/Contents/MacOS/openhuman-core" ]; then
echo "Unexpected standalone binary found inside app bundle: openhuman-core"
echo "Unexpected standalone core binary found in MacOS dir"
exit 1
fi
if ! ls "$APP_PATH/Contents/Resources"/openhuman-core-* >/dev/null 2>&1; then
echo "Sidecar core binary missing from app resources"
exit 1
fi
- name: Build standalone CLI binaries
- name: Build standalone CLI binary
shell: bash
run: |
# Temporarily inject [[bin]] entries that were removed from Cargo.toml
# to prevent Tauri's bundler from trying to copy them into app bundles.
printf '\n[[bin]]\nname = "openhuman-core"\npath = "src/standalone/openhuman-core.rs"\n\n[[bin]]\nname = "openhuman-cli"\npath = "src/standalone/openhuman-cli.rs"\n' >> src-tauri/Cargo.toml
CARGO_TARGET=""
if [ -n "$MATRIX_TARGET" ]; then
CARGO_TARGET="--target $MATRIX_TARGET"
fi
cargo build --manifest-path src-tauri/Cargo.toml --release --features standalone-bins $CARGO_TARGET --bin openhuman-core --bin openhuman-cli
cargo build --manifest-path rust-core/Cargo.toml --release --target "$MATRIX_TARGET" --bin openhuman-cli
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
- name: Resolve standalone CLI artifact paths
- name: Resolve standalone CLI artifact path
id: cli-paths
shell: bash
run: |
if [ -n "$MATRIX_TARGET" ]; then
BASE_DIR="src-tauri/target/$MATRIX_TARGET/release"
else
BASE_DIR="src-tauri/target/release"
fi
BASE_DIR="rust-core/target/$MATRIX_TARGET/release"
echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
echo "core_path=$BASE_DIR/openhuman-core" >> "$GITHUB_OUTPUT"
echo "cli_path=$BASE_DIR/openhuman-cli" >> "$GITHUB_OUTPUT"
env:
MATRIX_TARGET: ${{ matrix.settings.target }}
@@ -382,7 +396,6 @@ jobs:
with:
name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
path: |
${{ steps.cli-paths.outputs.core_path }}
${{ steps.cli-paths.outputs.cli_path }}
cleanup-failed-release:
+1
View File
@@ -51,3 +51,4 @@ coverage/
tauri.key
tauri.key.pub
/target/
+6
View File
@@ -2,6 +2,12 @@
Cross-platform crypto community communication platform built with **Tauri v2** (React 19 + Rust). Targets desktop (Windows, macOS) and mobile (Android, iOS). Features deep Telegram integration via MTProto, real-time Socket.io communication, V8-based skill execution engine, and an MCP (Model Context Protocol) tool system for AI-driven Telegram interactions.
## Runtime Scope
- Tauri host/runtime is desktop-only.
- Always run and validate Tauri codepaths as desktop (`windows`, `macOS`, `linux`).
- Do not add or maintain Android/iOS/web runtime branches inside `src-tauri`.
## App Theme & Design System
**Design Philosophy**: Premium, sophisticated crypto platform with calm, trustworthy aesthetic.
Generated
+11298
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
[workspace]
members = ["rust-core", "src-tauri"]
resolver = "2"
+3
View File
@@ -22,9 +22,12 @@ export default [
{
ignores: [
'node_modules/**',
'target/**',
'**/target/**',
'dist/**',
'coverage/**',
'src-tauri/**',
'rust-core/**',
'skills/**',
'references/**',
'scripts/**',
-2
View File
@@ -20,8 +20,6 @@
"macos:build:sign:release": "yarn macos:build:release:signed",
"macos:run": "open 'src-tauri/target/debug/bundle/macos/OpenHuman.app'",
"macos:dev": "yarn macos:build:debug && open 'src-tauri/target/debug/bundle/macos/OpenHuman.app'",
"android:dev": "tauri android dev",
"android:build": "tauri android build",
"test": "vitest run --config test/vitest.config.ts",
"test:unit": "vitest run --config test/vitest.config.ts",
"test:unit:watch": "vitest --config test/vitest.config.ts",
+110
View File
@@ -0,0 +1,110 @@
[package]
name = "openhuman-core"
version = "0.49.16"
edition = "2021"
description = "OpenHuman core business logic and RPC server"
autobins = true
[lib]
name = "openhuman_core"
crate-type = ["rlib"]
[dependencies]
tauri = { version = "2.10", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls", "native-tls", "stream", "http2", "multipart", "socks"] }
tokio = { version = "1", features = ["full", "sync"] }
once_cell = "1.19"
parking_lot = "0.12"
log = "0.4"
env_logger = "0.11"
base64 = "0.22"
aes-gcm = "0.10"
argon2 = "0.5"
rand = "0.9"
dirs = "5"
sha2 = "0.10"
hmac = "0.12"
uuid = { version = "1", features = ["v4"] }
anyhow = "1.0"
async-trait = "0.1"
chacha20poly1305 = "0.10"
hex = "0.4"
tokio-util = { version = "0.7", features = ["rt"] }
landlock = { version = "0.4", optional = true }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] }
futures = "0.3"
rusqlite = { version = "0.37", features = ["bundled"] }
chrono = { version = "0.4", features = ["serde"] }
cron = "0.12"
futures-util = "0.3"
directories = "6"
toml = "1.0"
shellexpand = "3.1"
schemars = "1.2"
tracing = { version = "0.1", default-features = false }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter"] }
prometheus = { version = "0.14", default-features = false }
urlencoding = "2.1"
thiserror = "2.0"
ring = "0.17"
prost = { version = "0.14", default-features = false }
postgres = { version = "0.19", features = ["with-chrono-0_4"] }
chrono-tz = "0.10"
dialoguer = { version = "0.12", features = ["fuzzy-select"] }
console = "0.16"
glob = "0.3"
regex = "1.10"
hostname = "0.4.2"
rustls = { version = "0.23", features = ["ring"] }
rustls-pki-types = "1.14.0"
tokio-rustls = "0.26.4"
webpki-roots = "1.0.6"
clap = { version = "4.5", features = ["derive"] }
lettre = { version = "0.11.19", default-features = false, features = ["builder", "smtp-transport", "rustls-tls"] }
mail-parser = "0.11.2"
async-imap = { version = "0.11", features = ["runtime-tokio"], default-features = false }
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "query", "ws", "macros"] }
tower = { version = "0.5", default-features = false }
tower-http = { version = "0.6", default-features = false, features = ["limit", "timeout"] }
http-body-util = "0.1"
opentelemetry = { version = "0.31", default-features = false, features = ["trace", "metrics"] }
opentelemetry_sdk = { version = "0.31", default-features = false, features = ["trace", "metrics"] }
opentelemetry-otlp = { version = "0.31", default-features = false, features = ["trace", "metrics", "http-proto", "reqwest-client", "reqwest-rustls-webpki-roots"] }
tokio-stream = { version = "0.1.18", features = ["full"] }
url = "2"
tinyhumansai = "0.1.6"
rquickjs = { version = "0.11", features = ["futures", "macro", "loader", "parallel"], optional = true }
matrix-sdk = { version = "0.16", optional = true, default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] }
fantoccini = { version = "0.22.0", optional = true, default-features = false, features = ["rustls-tls"] }
serde-big-array = { version = "0.5", optional = true }
nusb = { version = "0.2", default-features = false, optional = true }
tokio-serial = { version = "5", default-features = false, optional = true }
rppal = { version = "0.22", optional = true }
probe-rs = { version = "0.30", optional = true }
pdf-extract = { version = "0.10", optional = true }
wa-rs = { version = "0.2", optional = true, default-features = false }
wa-rs-core = { version = "0.2", optional = true, default-features = false }
wa-rs-binary = { version = "0.2", optional = true, default-features = false }
wa-rs-proto = { version = "0.2", optional = true, default-features = false }
wa-rs-ureq-http = { version = "0.2", optional = true }
wa-rs-tokio-transport = { version = "0.2", optional = true, default-features = false }
[dev-dependencies]
tempfile = "3"
[features]
tauri-host = ["dep:tauri", "dep:rquickjs"]
sandbox-landlock = ["dep:landlock"]
sandbox-bubblewrap = []
hardware = ["dep:nusb", "dep:tokio-serial"]
channel-matrix = ["dep:matrix-sdk"]
peripheral-rpi = ["dep:rppal"]
browser-native = ["dep:fantoccini"]
fantoccini = ["browser-native"]
landlock = ["sandbox-landlock"]
probe = ["dep:probe-rs"]
rag-pdf = ["dep:pdf-extract"]
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "serde-big-array"]
@@ -19,14 +19,14 @@ items that are:
Return a JSON array of actionable items. Each item must have this exact structure:
{
"title": "Short descriptive title (under 80 chars)",
"description": "1-2 sentence explanation with context",
"source": "email|calendar|telegram|ai_insight|system|trading|security",
"priority": "critical|important|normal",
"actionable": true,
"requires_confirmation": false,
"has_complex_action": false,
"source_label": "Human-readable source name (e.g. Gmail, Telegram, Notion)"
"title": "Short descriptive title (under 80 chars)",
"description": "1-2 sentence explanation with context",
"source": "email|calendar|telegram|ai_insight|system|trading|security",
"priority": "critical|important|normal",
"actionable": true,
"requires_confirmation": false,
"has_complex_action": false,
"source_label": "Human-readable source name (e.g. Gmail, Telegram, Notion)"
}
## Rules
@@ -310,24 +310,24 @@ fn execute(cli: Cli) -> Result<serde_json::Value, String> {
}
}
fn load_config() -> Result<openhuman::openhuman::config::Config, String> {
fn load_config() -> Result<openhuman_core::openhuman::config::Config, String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("failed to build runtime: {e}"))?;
runtime
.block_on(openhuman::openhuman::config::Config::load_or_init())
.block_on(openhuman_core::openhuman::config::Config::load_or_init())
.map_err(|e| format!("failed to load config: {e}"))
}
fn status_value(
status: openhuman::openhuman::service::ServiceStatus,
status: openhuman_core::openhuman::service::ServiceStatus,
) -> Result<serde_json::Value, String> {
serde_json::to_value(status).map_err(|e| format!("failed to serialize service status: {e}"))
}
fn command_response_json(
status: openhuman::openhuman::service::ServiceStatus,
status: openhuman_core::openhuman::service::ServiceStatus,
log: &str,
) -> Result<serde_json::Value, String> {
Ok(json!({
@@ -338,35 +338,35 @@ fn command_response_json(
fn local_service_install() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = openhuman::openhuman::service::install(&config)
let status = openhuman_core::openhuman::service::install(&config)
.map_err(|e| format!("service install failed: {e}"))?;
command_response_json(status, "service install completed")
}
fn local_service_start() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = openhuman::openhuman::service::start(&config)
let status = openhuman_core::openhuman::service::start(&config)
.map_err(|e| format!("service start failed: {e}"))?;
command_response_json(status, "service start completed")
}
fn local_service_stop() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = openhuman::openhuman::service::stop(&config)
let status = openhuman_core::openhuman::service::stop(&config)
.map_err(|e| format!("service stop failed: {e}"))?;
command_response_json(status, "service stop completed")
}
fn local_service_status() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = openhuman::openhuman::service::status(&config)
let status = openhuman_core::openhuman::service::status(&config)
.map_err(|e| format!("service status failed: {e}"))?;
command_response_json(status, "service status fetched")
}
fn local_service_uninstall() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = openhuman::openhuman::service::uninstall(&config)
let status = openhuman_core::openhuman::service::uninstall(&config)
.map_err(|e| format!("service uninstall failed: {e}"))?;
command_response_json(status, "service uninstall completed")
}
@@ -1,6 +1,6 @@
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
if let Err(err) = openhuman::core_server::run_from_cli_args(&args) {
if let Err(err) = openhuman_core::core_server::run_from_cli_args(&args) {
eprintln!("openhuman-core failed: {err}");
std::process::exit(1);
}
+12
View File
@@ -0,0 +1,12 @@
#[cfg(feature = "tauri-host")]
pub mod ai;
pub mod auth;
pub mod core_server;
pub mod memory;
pub mod models;
pub mod openhuman;
pub mod runtime;
pub fn run_core_from_args(args: &[String]) -> anyhow::Result<()> {
core_server::run_from_cli_args(args)
}
+453
View File
@@ -0,0 +1,453 @@
//! TinyHumans Neocortex persistent memory layer.
//!
//! Wraps `TinyHumanMemoryClient` with helpers for skill data-sync.
//! The client is initialised at runtime with the user's JWT token (from Redux
//! `authSlice.token`) via the `init_memory_client` Tauri command, not from env vars.
use std::sync::Arc;
use tinyhumansai::{
DeleteMemoryParams, InsertMemoryParams, Priority, QueryMemoryParams, RecallMemoryParams,
SourceType, TinyHumanConfig, TinyHumansMemoryClient,
};
use uuid::Uuid;
/// Shared, cloneable handle to the memory client.
pub type MemoryClientRef = Arc<MemoryClient>;
/// Shared app-state slot for the memory client.
pub struct MemoryState(pub std::sync::Mutex<Option<MemoryClientRef>>);
pub struct MemoryClient {
inner: TinyHumansMemoryClient,
}
impl MemoryClient {
/// Construct from a JWT token (sourced from `authSlice.token` in the Redux store).
/// Returns `None` if the token is empty or client construction fails.
pub fn from_token(jwt_token: String) -> Option<Self> {
log::info!(
"[memory] from_token: entry (token_len={})",
jwt_token.trim().len()
);
if jwt_token.trim().is_empty() {
log::warn!("[memory] from_token: exit — token is empty, returning None");
return None;
}
let config = if let Ok(base_url) =
std::env::var("OPENHUMAN_BASE_URL").or_else(|_| std::env::var("TINYHUMANS_BASE_URL"))
{
log::info!(
"[memory] from_token: constructing client (base_url={base_url}, source=memory_env)"
);
TinyHumanConfig::new(jwt_token).with_base_url(base_url)
} else {
let backend_url = std::env::var("VITE_BACKEND_URL")
.ok()
.filter(|url| !url.trim().is_empty())
.unwrap_or_else(|| "http://localhost:5005".to_string());
log::info!(
"[memory] from_token: constructing client (base_url={backend_url}, source=fallback_env_default)"
);
TinyHumanConfig::new(jwt_token).with_base_url(backend_url)
};
match TinyHumansMemoryClient::new(config) {
Ok(inner) => {
log::info!("[memory] from_token: exit — client created successfully");
Some(Self { inner })
}
Err(e) => {
log::warn!("[memory] from_token: exit — client construction failed: {e}");
None
}
}
}
/// Store a skill data-sync result.
///
/// Inserts the document then polls `ingestion_job_status` every 30 s until
/// the job reaches `completed` (or `failed`/`error`). Returns only after the
/// ingestion job is confirmed complete.
pub async fn store_skill_sync(
&self,
skill_id: &str,
integration_id: &str,
title: &str,
content: &str,
source_type: Option<SourceType>,
metadata: Option<serde_json::Value>,
priority: Option<Priority>,
created_at: Option<f64>,
updated_at: Option<f64>,
document_id: Option<String>,
) -> Result<(), String> {
let namespace = skill_id.to_string();
log::info!(
"[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})",
content.len()
);
let document_id_final = document_id.unwrap_or_else(|| Uuid::new_v4().to_string());
log::info!(
"[memory] insert_memory: calling SDK (namespace={namespace}, title={title:?}), content_len={}",
content.len()
);
let insert_resp = self
.inner
.insert_memory(InsertMemoryParams {
document_id: document_id_final,
title: title.to_string(),
content: content.to_string(),
namespace: namespace.clone(),
source_type,
metadata,
priority,
created_at,
updated_at,
..Default::default()
})
.await
.map_err(|e| {
log::warn!(
"[memory] insert_memory: SDK error — kind={:?} msg={e}",
classify_insert_error(&e)
);
format!("Memory insert failed: {e}")
})?;
log::info!(
"[memory] insert_memory: accepted (namespace={namespace}, status={:?}, job_id={:?})",
insert_resp.data.status,
insert_resp.data.job_id
);
// If the API returned a job_id, poll until the job completes.
if let Some(job_id) = insert_resp.data.job_id {
log::info!("[memory] ingestion job queued (job_id={job_id}), polling every 30s...");
loop {
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
match self.inner.get_ingestion_job(&job_id).await {
Ok(status_resp) => {
let state = status_resp.data.state.as_deref().unwrap_or("unknown");
log::info!(
"[memory] ingestion job status: job_id={job_id}, state={state}, \
attempts={:?}, completed_at={:?}",
status_resp.data.attempts,
status_resp.data.completed_at
);
match state {
"completed" => {
log::info!(
"[memory] ingestion job completed (job_id={job_id}, namespace={namespace})"
);
break;
}
"failed" | "error" => {
let err_msg = status_resp
.data
.error
.unwrap_or_else(|| format!("job state={state}"));
log::warn!(
"[memory] ingestion job failed: job_id={job_id}, error={err_msg}"
);
log::warn!(
"[memory] store_skill_sync: exit — ingestion failed (namespace={namespace})"
);
return Err(format!("Ingestion job failed: {err_msg}"));
}
_ => {
// pending / processing / queued — keep waiting
log::info!(
"[memory] ingestion job still in progress (state={state}), waiting 30s..."
);
}
}
}
Err(e) => {
log::warn!(
"[memory] ingestion job status poll error (job_id={job_id}): {e} — retrying in 30s"
);
}
}
}
} else {
log::info!("[memory] no job_id returned — insert assumed synchronous, proceeding");
}
log::info!("[memory] store_skill_sync: exit — ok (namespace={namespace})");
Ok(())
}
/// Query relevant context for a skill integration (RAG).
pub async fn query_skill_context(
&self,
skill_id: &str,
_integration_id: &str,
query: &str,
max_chunks: u32,
) -> Result<String, String> {
let namespace = skill_id.to_string();
log::info!("[memory] query_skill_context: entry (namespace={namespace}, max_chunks={max_chunks}, query={query:?})");
log::debug!(
"[memory] query_skill_context: payload → namespace={namespace} | max_chunks={max_chunks} | query={query}"
);
let res = self
.inner
.query_memory(QueryMemoryParams {
query: query.to_string(),
namespace: Some(namespace.clone()),
max_chunks: Some(f64::from(max_chunks)),
..Default::default()
})
.await
.map_err(|e| {
log::warn!(
"[memory] query_skill_context: exit — error (namespace={namespace}): {e}"
);
format!("Memory query failed: {e}")
})?;
let response = res.data.response.unwrap_or_default();
log::info!(
"[memory] query_skill_context: exit — ok (namespace={namespace}, response_len={})",
response.len()
);
Ok(response)
}
/// Recall context from the Master memory node for a given namespace.
/// Returns the synthesised `response` string, or `None` if the server returned nothing.
pub async fn recall_skill_context(
&self,
skill_id: &str,
_integration_id: &str,
max_chunks: u32,
) -> Result<Option<serde_json::Value>, String> {
let namespace = skill_id.to_string();
log::info!(
"[memory] recall_skill_context: entry (namespace={namespace}, max_chunks={max_chunks})"
);
let res = self
.inner
.recall_memory(RecallMemoryParams {
namespace: Some(namespace.clone()),
max_chunks: Some(f64::from(max_chunks)),
})
.await
.map_err(|e| {
log::warn!(
"[memory] recall_skill_context: exit — error (namespace={namespace}): {e}"
);
format!("Memory recall failed: {e}")
})?;
let response = res.data.context;
log::info!(
"[memory] recall_skill_context: exit — ok (namespace={namespace}, has_response={})",
response.is_some()
);
Ok(response)
}
/// List all ingested memory documents as returned by the API.
pub async fn list_documents(&self) -> Result<serde_json::Value, String> {
self.inner
.list_documents(tinyhumansai::ListDocumentsParams::default())
.await
.map_err(|e| format!("Memory list documents failed: {e}"))
}
/// Delete a specific document from a namespace.
pub async fn delete_document(
&self,
document_id: &str,
namespace: &str,
) -> Result<serde_json::Value, String> {
self.inner
.delete_document(document_id, namespace)
.await
.map_err(|e| format!("Memory delete document failed: {e}"))
}
/// Query memory context by namespace directly.
pub async fn query_namespace_context(
&self,
namespace: &str,
query: &str,
max_chunks: u32,
) -> Result<String, String> {
let res = self
.inner
.query_memory(QueryMemoryParams {
query: query.to_string(),
namespace: Some(namespace.to_string()),
max_chunks: Some(f64::from(max_chunks)),
..Default::default()
})
.await
.map_err(|e| format!("Memory query failed: {e}"))?;
Ok(res.data.response.unwrap_or_default())
}
/// Recall memory context by namespace directly.
pub async fn recall_namespace_context(
&self,
namespace: &str,
max_chunks: u32,
) -> Result<Option<String>, String> {
let res = self
.inner
.recall_memory(RecallMemoryParams {
namespace: Some(namespace.to_string()),
max_chunks: Some(f64::from(max_chunks)),
})
.await
.map_err(|e| format!("Memory recall failed: {e}"))?;
Ok(res.data.response)
}
/// Delete all memories for a skill integration (e.g. on OAuth revoke).
pub async fn clear_skill_memory(
&self,
skill_id: &str,
_integration_id: &str,
) -> Result<(), String> {
let namespace = skill_id.to_string();
log::info!("[memory] clear_skill_memory: entry (namespace={namespace})");
log::debug!("[memory] clear_skill_memory: payload → namespace={namespace}");
let result = self
.inner
.delete_memory(DeleteMemoryParams {
namespace: Some(namespace.clone()),
})
.await
.map(|_| ())
.map_err(|e| format!("Memory delete failed: {e}"));
match &result {
Ok(()) => log::info!("[memory] clear_skill_memory: exit — ok (namespace={namespace})"),
Err(e) => {
log::warn!("[memory] clear_skill_memory: exit — error (namespace={namespace}): {e}")
}
}
result
}
}
fn classify_insert_error(e: &tinyhumansai::TinyHumansError) -> &'static str {
let msg = e.to_string();
if msg.contains("dns") || msg.contains("resolve") || msg.contains("lookup") {
"dns_failure"
} else if msg.contains("tls") || msg.contains("certificate") || msg.contains("ssl") {
"tls_failure"
} else if msg.contains("Connection refused") || msg.contains("connection refused") {
"connection_refused"
} else if msg.contains("timed out") || msg.contains("deadline") {
"timeout"
} else if msg.contains("error sending request") {
"transport_error"
} else {
"other"
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Integration test against the real TinyHumans production API.
///
/// Verifies: JWT is accepted, endpoint is reachable, and request shape is correct.
/// A `400 Insufficient ingestion budget` response is treated as a PASS because it proves:
/// - auth succeeded (not 401/403)
/// - the endpoint and payload are correctly formed (not 422)
/// - the account quota is the only limiting factor
///
/// Run with:
/// JWT_TOKEN=<your-openhuman-jwt> \
/// cargo test --manifest-path src-tauri/Cargo.toml test_memory_skill_sync_flow -- --ignored --nocapture
#[tokio::test]
#[ignore]
async fn test_memory_skill_sync_flow() {
let jwt_token = std::env::var("JWT_TOKEN").expect("JWT_TOKEN must be set");
let client = MemoryClient::from_token(jwt_token).expect("client creation failed");
let skill_id = "gmail";
let integration_id = "test@openhuman.dev";
let dummy_content = serde_json::json!({
"integrationId": integration_id,
"type": "gmail_sync",
"summary": "Synced 45 emails from inbox",
"labels": ["Work", "Personal", "Finance"],
"recentSenders": ["alice@example.com", "bob@example.com"],
"unreadCount": 12,
"syncedAt": "2026-03-17T12:00:00Z"
});
// ── 1. Insert ─────────────────────────────────────────────────────────
let insert_result = client
.store_skill_sync(
skill_id,
integration_id,
"Gmail OAuth sync — test@openhuman.dev",
&serde_json::to_string_pretty(&dummy_content).unwrap(),
None,
None,
None,
None,
None,
None,
)
.await;
println!("[test] insert result: {insert_result:?}");
match &insert_result {
Ok(()) => {
println!("[test] ✓ INSERT succeeded — quota available");
// ── 2. Query ─────────────────────────────────────────────────
let context = client
.query_skill_context(
skill_id,
integration_id,
"What emails were recently synced and who sent them?",
10,
)
.await;
println!("[test] query result: {context:?}");
assert!(context.is_ok(), "query_memory failed: {context:?}");
println!("[test] memory context:\n{}", context.unwrap());
// ── 3. Cleanup ────────────────────────────────────────────────
let del = client.clear_skill_memory(skill_id, integration_id).await;
println!("[test] delete result: {del:?}");
assert!(del.is_ok(), "delete_memory failed: {del:?}");
}
Err(e) if e.contains("Insufficient ingestion budget") => {
// Account quota exhausted — auth + endpoint + payload all correct.
println!(
"[test] ✓ API REACHABLE — auth accepted, endpoint correct.\n\
Quota limited: {e}\n\
Integration is wired correctly; upgrade the TinyHumans account \
to enable full insert/query/delete flow."
);
// Not a code failure — pass the test.
}
Err(e) => {
panic!("Unexpected error (not a quota issue): {e}");
}
}
}
/// Smoke test: from_token() returns None for an empty token.
#[test]
fn test_from_token_returns_none_for_empty_token() {
assert!(MemoryClient::from_token(String::new()).is_none());
assert!(MemoryClient::from_token(" ".to_string()).is_none());
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod auth;
pub mod socket;
@@ -247,7 +247,7 @@ end tell"#
}
async fn health_check(&self) -> bool {
if !cfg!(target_os = "macos") {
if std::env::consts::OS != "macos" {
return false;
}

Some files were not shown because too many files have changed in this diff Show More