car-ffi-common 0.32.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
#![recursion_limit = "512"]

//! Shared logic for FFI bindings (NAPI, PyO3).
//!
//! Pure Rust functions that accept JSON strings and return `Result<String, String>`.
//! Each FFI crate wraps these with its own error type (napi::Error, PyErr).

/// Wire protocol version this binding speaks to the daemon (the
/// `server.handshake` negotiation value). Re-exported from `car-proto` so
/// the NAPI/PyO3 bindings can surface it to JS/Python consumers without a
/// direct `car-proto` dependency — and so there is exactly one source of
/// truth for the constant across the proxy, the daemon, and the bindings.
pub use car_proto::PROTOCOL_VERSION;
// Re-export so `car-cli` can reap OS-level schedules (e.g. in `car purge`)
// without a redundant direct `car-scheduler` dependency — car-cli already
// depends on car-ffi-common, which already depends on car-scheduler.
pub use car_scheduler::os_schedule;

/// Parse a JSON argument into `T`, labeling a parse failure with the argument
/// name (e.g. `from_json("graph", graph_json)` → `"invalid graph JSON: …"`).
/// The shared front half of the stateless JSON-wrapper pattern that pervades
/// this crate — see the module docs.
pub fn from_json<T: serde::de::DeserializeOwned>(label: &str, s: &str) -> Result<T, String> {
    serde_json::from_str(s).map_err(|e| format!("invalid {label} JSON: {e}"))
}

/// Serialize a value to a JSON string, mapping the (near-impossible) failure to
/// its message. The shared back half of the stateless JSON-wrapper pattern.
pub fn to_json<T: serde::Serialize>(value: &T) -> Result<String, String> {
    serde_json::to_string(value).map_err(|e| e.to_string())
}

/// Parse an *optional* JSON argument: `None` stays `None`, `Some(s)` is parsed
/// via [`from_json`] (so a malformed value is a labeled error, not a silent
/// `None`). The optional-argument companion to [`from_json`].
pub fn from_json_opt<T: serde::de::DeserializeOwned>(
    label: &str,
    s: Option<&str>,
) -> Result<Option<T>, String> {
    s.map(|x| from_json(label, x)).transpose()
}

/// Initialize a `tracing_subscriber` honoring `RUST_LOG`. Idempotent
/// — subsequent calls return immediately. Used by FFI bindings to
/// surface `[voice] dropped …` and other crate-side warn/error lines
/// when callers set `RUST_LOG=car_voice=debug` (or similar).
///
/// Without this call, `tracing::warn!`/`error!` lines emitted from
/// car-voice / car-inference / etc. go nowhere in the FFI binary —
/// `#120`'s diagnostic dead-end. We can't `init` from a binding's
/// `[lib]` ctor (no static init in cdylib that's safe for tracing),
/// so each entry-point function calls this and the OnceLock guard
/// makes it cheap.
///
/// Falls back silently if another global subscriber is already set
/// (e.g., when the embedder loads `car-server` and the FFI module
/// in the same process).
pub fn ensure_tracing_subscriber() {
    use std::sync::OnceLock;
    static INIT: OnceLock<()> = OnceLock::new();
    INIT.get_or_init(|| {
        let filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
        // try_init returns Err if a subscriber is already set; that's
        // fine, we just want at-most-one to win.
        let _ = tracing_subscriber::fmt()
            .with_env_filter(filter)
            .with_writer(std::io::stderr)
            .with_target(false)
            .try_init();
    });
}

// `auth_token` and `proxy` moved to the `car-daemon-client` crate so
// the UniFFI (Swift/Kotlin) binding can reach the daemon without
// absorbing this crate's transitive tree — car-voice (whisper-rs,
// coreaudio-sys), car-browser, car-vision are wrong for
// aarch64-apple-ios and unnecessary for a pure WS client. Re-exported
// here so every existing `car_ffi_common::proxy::…` /
// `car_ffi_common::auth_token::…` import path keeps working.
pub use car_daemon_client::{auth_token, proxy};

pub mod a2ui;
pub mod accounts;
pub mod automation;
pub mod browser;
pub mod cascade;
pub mod coder;
pub mod crdt;
pub mod cwm;
pub mod declagents;
pub mod discovery;
pub mod eviction;
pub mod external_agents;
pub mod harness_adapt;
pub mod harness_evolution;
pub mod harness_metrics;
pub mod health;
pub mod integrations;
pub mod maintenance;
pub mod memory_path;
pub mod memsys;
pub mod multi;
pub mod nlp;
pub mod notifications;
pub mod parslee;
pub mod permgate;
pub mod permissions;
pub mod projects;
pub mod registry;
pub mod scheduler;
pub mod secrets;
pub mod self_evolution;
pub mod skill_trust;
pub mod supervisor;
pub mod tool_receipts;
pub mod utility;
pub mod verify;
pub mod vision;
pub mod voice;
pub mod workflow;
// (moved in v0.8.x phase 7.3) `a2a`, `a2a_dispatch`, `meeting`,
// and `voice_turn` modules used to live here. They were daemon-only
// (FFI bindings now proxy through `proxy::*` over WebSocket), and
// their car-engine / car-inference / car-a2a / car-meeting deps
// were the last reason this crate pulled those heavy graphs. The
// moved modules now live in `car-server-core/src/` next to the
// JSON-RPC handler that calls them. `a2a_dispatch` was deleted
// outright (no callers — the daemon's WS routes use their own
// `ServerState`-held dispatcher).

// (removed in v0.8) tracked_result_to_json — was a typed
// InferenceResult → JSON helper for FFI bindings. After phase 7.2,
// FFI binding methods build requests as raw json! and pass through
// the daemon's full InferenceResult JSON unmodified. The helper
// pulled car-inference (and transitively MLX) for what amounts to a
// thin field re-projection, so it's gone.

/// Process-wide serialization lock for tests that mutate environment
/// variables. The process environment is global, so any test that
/// `set_var`/`remove_var`s — or that *reads* an env-derived path while
/// another test mutates it — must hold this. Previously each module
/// (`auth_token`, `memory_path`, `proxy`) had its OWN lock, so e.g. an
/// `auth_token` test reading `HOME` raced a `memory_path` test writing
/// `HOME`; a panic under one lock also poisoned it and cascaded the
/// failure across that module's sibling tests. A single shared lock
/// removes the cross-module race, and recovering the guard on poison
/// (`into_inner`) stops one failure from cascading into many.
#[cfg(test)]
pub(crate) fn env_test_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}