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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ jobs:
run: |
bash scripts/ci-cancel-aware.sh cargo test --manifest-path Cargo.toml \
--no-default-features --features tokenjuice-treesitter --lib -- \
core::all:: core::cli:: core::jsonrpc:: core::legacy_aliases:: core::runtime:: agent::registry::agents::loader:: tools::schemas:: tools::ops::tests::
core::all:: core::cli:: core::jsonrpc:: core::legacy_aliases:: core::runtime:: agent::registry::agents::loader:: memory::people::address_book::tests:: tools::schemas:: tools::ops::tests::
bash scripts/ci-cancel-aware.sh cargo test --manifest-path Cargo.toml \
--no-default-features --features tokenjuice-treesitter,mcp --lib -- \
mcp::server::resources::
Expand Down Expand Up @@ -498,6 +498,7 @@ jobs:
openhuman/agent/harness/subagent_runner/tool_prep.rs
openhuman/agent/registry/agents/loader.rs
openhuman/inference/local/mod.rs
openhuman/memory/people/address_book.rs
openhuman/mcp/server/resources.rs
openhuman/platform/socket/ops.rs
openhuman/tinyplace/manifest.rs
Expand All @@ -511,7 +512,7 @@ jobs:
openhuman/web3/x402/stub.rs
EOF
)
ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows|channels|prediction-markets)"' src --include='*.rs' \
ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows|channels|prediction-markets|contacts)"|#\[cfg\((not\()?all\([^]]*feature = "contacts"' src --include='*.rs' \
| xargs grep -lE '#\[test\]|#\[tokio::test\]|fn .*_test' 2>/dev/null | sed 's|^src/||' | sort -u)
if ! diff <(echo "$EXPECTED" | sed 's/^ *//' | sort -u) <(echo "$ACTUAL"); then
echo "::error::Gated-test file set changed. Update the EXPECTED allowlist in the rust-feature-gate-smoke lane, and extend the scoped 'cargo test' filter if the new module can carry an ungated-assert regression (see #5022)."
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ whole cohort or expect a delta of 0.
| `mcp` | ON | `openhuman::mcp::server` (the `openhuman mcp` stdio/HTTP server), `openhuman::mcp::registry` (dynamic Smithery installs — `mcp_clients` RPC namespace, SQLite, boot spawn, supervisor, OAuth), `openhuman::mcp::audit` (write-audit log), and the static config-declared server set in `openhuman::mcp::config_servers`. ~19 agent tools, ~20k LOC | **none** (see scope note) |
| `tui` | ON | `openhuman::tui` — the tabbed ratatui/crossterm CLI UI (Logs, Chat, Config, Settings), auto-opened by bare `openhuman` on interactive non-container hosts and forced with `openhuman tui` (alias `chat`). Runs the core in-process. No controllers, no agent tools. **Intentionally NOT forwarded to the desktop shell** (allowlisted in `check-feature-forwarding.mjs`). | `ratatui`, `crossterm` |
| `channels` | ON | `openhuman::channels` (external-messaging providers — Telegram/Discord/Slack/Signal/WhatsApp/iMessage/IRC/… — plus the channel runtime, controllers, host, proactive messaging + inbound dispatch) and the `channels::webview_accounts` / `webview_apis` / `webview_notifications` / `channels::whatsapp_data` webview-bridge domains (incl. the 3 `whatsapp_data_*` agent tools). **Carve-outs `channels::{traits, cli}` stay ungated.** | **28** via `tinychannels/{email,lark}` — the crate itself stays (load-bearing), its two heavy providers do not |
| `contacts` | ON | `memory::people::address_book`'s macOS CNContactStore reader — the address-book seeding path for the people domain. Leaf gate over a **pre-existing** off-state: the module already shipped a non-macOS `imp` stub returning an empty contact list, so the gate only widens that stub's cfg. `read`/`read_with`/`AddressBookError`/`SystemContactsSource` and the whole `people` RPC surface stay compiled in every build; off ⇒ a refresh seeds nothing instead of failing. | **6** on macOS (`objc2`, `objc2-foundation`, `objc2-contacts`, `block2` + 2 transitive). **No-op on Linux/Windows** — never in those graphs, so the kernel-floor ratchet does not move. Verify cross-target: `cargo tree --target aarch64-apple-darwin -e normal -i objc2-contacts --no-default-features --features tokenjuice-treesitter` (294 → 288 packages). |

**Facade pattern (pathfinder for the other gates).** `pub mod voice;` is **always compiled** as a facade: the real submodules are `#[cfg(feature = "voice")]`, and a `#[cfg(not(feature = "voice"))] mod stub;` (`src/openhuman/voice/stub.rs`) re-exposes the same public surface that always-on / other-gated callers use (`server`, `dictation_listener`, `streaming`, `reply_speech`, `cloud_transcribe`, `cli`, `create_stt_provider`, `effective_stt_provider`, `publish_ptt_transcript_committed`) with no-op / `None` / disabled-error bodies. Callers therefore do **not** need per-call `#[cfg]`. When voice is off: the voice/audio controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), the `audio_generate_podcast` agent tools are absent, and `openhuman voice` returns a "voice disabled" error. Stub signatures must match the real ones exactly — the disabled build (`--no-default-features --features tokenjuice-treesitter`) is the **only** thing that catches drift, so run it before pushing any change to the voice surface.

Expand Down
31 changes: 25 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,13 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c

[target.'cfg(target_os = "macos")'.dependencies]
whisper-rs = { version = "0.16", features = ["metal"], optional = true }
# Contacts framework bindings for address book seeding.
objc2 = "0.6"
objc2-foundation = { version = "0.3", features = ["NSArray", "NSError", "NSObject", "NSString", "NSPredicate"] }
objc2-contacts = { version = "0.3.2", features = ["CNContact", "CNContactFetchRequest", "CNContactStore", "CNLabeledValue", "CNPhoneNumber"] }
block2 = "0.6"
# Contacts framework bindings for address book seeding. Exclusive to
# `memory::people::address_book` (verified: no other file in src/ names any of
# the four), so the default-ON `contacts` feature sheds the whole cohort.
objc2 = { version = "0.6", optional = true }
objc2-foundation = { version = "0.3", features = ["NSArray", "NSError", "NSObject", "NSString", "NSPredicate"], optional = true }
objc2-contacts = { version = "0.3.2", features = ["CNContact", "CNContactFetchRequest", "CNContactStore", "CNLabeledValue", "CNPhoneNumber"], optional = true }
block2 = { version = "0.6", optional = true }

[target.'cfg(target_os = "linux")'.dependencies]
landlock = { version = "0.4", optional = true }
Expand Down Expand Up @@ -398,7 +400,7 @@ tokio = { version = "1", features = ["test-util"] }
proptest = "1"

[features]
default = ["tokenjuice-treesitter", "inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "channels", "tui", "medulla", "scheduler-gate", "file-logging", "prediction-markets"]
default = ["tokenjuice-treesitter", "inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "channels", "tui", "medulla", "scheduler-gate", "file-logging", "prediction-markets", "contacts"]
# HTTP + Socket.IO server transport (#5048): the `/rpc` JSON-RPC endpoint and
# its auth middleware/CORS layer (`core::jsonrpc`, `core::auth`), the `/v1`
# OpenAI-compatible router (`inference::http`), the ad-hoc static-dir file
Expand Down Expand Up @@ -508,6 +510,23 @@ web3 = [
"dep:coins-bip39",
]

# macOS Contacts seeding for the people domain: `memory::people::address_book`
# reads CNContactStore to seed handles. Default-ON. Slim / headless builds opt
# out via `--no-default-features --features "<list without contacts>"`, which
# sheds the exclusive `objc2` / `objc2-foundation` / `objc2-contacts` / `block2`
# cohort on macOS. Leaf gate with a pre-existing off-state: `address_book.rs`
# already carries a non-macOS `imp` stub returning an empty contact list, so the
# gate simply widens that stub's cfg — `read()`, `read_with()`,
# `AddressBookError` and `SystemContactsSource` stay compiled in every build and
# the `people` RPC surface is unchanged. Off ⇒ address-book refresh seeds
# nothing instead of failing.
#
# NOTE: no-op on Linux/Windows, where these crates were never in the graph.
# Verify the shed cross-target from any host:
# cargo tree --target aarch64-apple-darwin -e normal -i objc2-contacts \
# --no-default-features --features tokenjuice-treesitter
contacts = ["dep:objc2", "dep:objc2-foundation", "dep:objc2-contacts", "dep:block2"]

# Polymarket prediction-market tools (read + trading). Implies `web3` because it
# signs orders with the same EVM stack the wallet uses — an honest dependency
# rather than a hidden one. Separate from `web3` so a build can have a wallet
Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ openhuman_core = { path = "../..", package = "openhuman", default-features = fal
# forwarded — but the checker does literal set membership with no
# transitive resolution, so this needs its own line.
"prediction-markets",
"contacts",
] }
tinyjuice = { version = "0.2.1", default-features = false }

Expand Down
20 changes: 15 additions & 5 deletions src/openhuman/memory/people/address_book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ pub fn read() -> Result<Vec<AddressBookContact>, AddressBookError> {
}

// ── macOS implementation ──────────────────────────────────────────────────────
//
// Gated on `contacts` as well as the target: the four objc2 crates this needs
// are exclusive to this module, so a slim macOS build sheds the whole cohort.

#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", feature = "contacts"))]
mod imp {
use super::{AddressBookContact, AddressBookError};

Expand Down Expand Up @@ -269,9 +272,14 @@ mod imp {
}
}

// ── non-macOS stub ────────────────────────────────────────────────────────────
// ── stub: non-macOS, or macOS with `contacts` compiled out ───────────────────
//
// Pre-dates the gate — it already existed for Linux/Windows. Widening its cfg
// is the whole off-state: `read()`, `read_with()`, `AddressBookError` and
// `SystemContactsSource` stay compiled everywhere, so the `people` RPC surface
// is identical and an address-book refresh seeds nothing rather than failing.

#[cfg(not(target_os = "macos"))]
#[cfg(not(all(target_os = "macos", feature = "contacts")))]
mod imp {
use super::{AddressBookContact, AddressBookError};

Expand Down Expand Up @@ -344,13 +352,15 @@ pub mod tests {

#[test]
fn system_source_non_mac_returns_empty() {
#[cfg(not(target_os = "macos"))]
// Mirrors the `imp` cfgs above: the stub is what compiles whenever the
// real CNContactStore path is absent, whether by target or by gate.
#[cfg(not(all(target_os = "macos", feature = "contacts")))]
Comment thread
senamakel marked this conversation as resolved.
{
let source = SystemContactsSource;
let result = read_with(&source).unwrap();
assert!(result.is_empty());
}
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", feature = "contacts"))]
{
// TCC state is environment-dependent; just verify no panic.
let source = SystemContactsSource;
Expand Down
26 changes: 7 additions & 19 deletions tests/raw_coverage/app_state_credentials_raw_coverage_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ async fn snapshot_activates_user_dir_after_pending_revalidation_without_initial_
}

#[tokio::test]
async fn snapshot_preserves_pending_session_when_revalidated_user_activation_fails() {
async fn snapshot_errors_without_clearing_pending_session_when_active_user_marker_is_unreadable() {
let _lock = env_lock();
let (api_url, server_task, shutdown_tx) = auth_me_server(
r#"{"data":{"id":"fresh-activation-failure","name":"Activation Failure","email":"activation-failure@example.test"}}"#,
Expand Down Expand Up @@ -863,28 +863,16 @@ async fn snapshot_preserves_pending_session_when_revalidated_user_activation_fai
)
.expect("seed activation-failure pending app session");

let snap = snapshot()
let error = snapshot()
.await
.expect("snapshot with failed pending activation")
.value;
.expect_err("unreadable active_user.toml must stop snapshot configuration resolution");
assert!(
snap.auth.is_authenticated,
"activation write failure should keep the pending local session for retry"
);
assert_eq!(
snap.session_token.as_deref(),
Some("round14.pending.activation-failure")
);
assert_eq!(
snap.current_user
.as_ref()
.and_then(|v| v.get("pendingBackendValidation")),
Some(&json!(true)),
"failed activation must not return a validated backend user"
error.contains("read active user marker"),
"snapshot must surface the unreadable marker instead of resolving to the pre-login profile: {error}"
);
assert!(
active_user_root.join("active_user.toml").is_dir(),
"failed active_user.toml write must not be treated as an activated user"
"unreadable active_user.toml must remain in place"
);

let profile = AuthService::from_config(&config)
Expand All @@ -901,7 +889,7 @@ async fn snapshot_preserves_pending_session_when_revalidated_user_activation_fai
assert_eq!(
stored_user.get("pendingBackendValidation"),
Some(&json!(true)),
"activation failure must not clear persisted pendingBackendValidation"
"configuration-resolution failure must not clear persisted pendingBackendValidation"
);

let _ = shutdown_tx.send(());
Expand Down
Loading