mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 22:23:01 +00:00
+16




![github-actions[bot] <github-actions[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)




Mega Mind
GitHub
YellowSnnowmann
Steven Enamakel
github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Cyrus Gray
Horst1993
Cursor
James Gentes
Sam
Sami Rusani
oxoxDev
Muhammad Ismail
Claude Fable 5
nb213
binyangzhu000-sudo
Steven Enamakel
CodeGhost21
sanil-23
M3gA-Mind
oxoxDev
mysma-9403
mwakidenis
NgoQuocViet2001
viet.ngo
Maciej Myszkiewicz
a40fbb79d2
Co-authored-by: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Co-authored-by: Horst1993 <horst.w@gmicloud.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: James Gentes <jgentes@users.noreply.github.com> Co-authored-by: Sam <samrusani@users.noreply.github.com> Co-authored-by: Sami Rusani <14844597+samrusani@users.noreply.github.com> Co-authored-by: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Co-authored-by: Muhammad Ismail <78064250+myi1@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: nb213 <binyangzhu000@gmail.com> Co-authored-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> Co-authored-by: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Co-authored-by: sanil-23 <sanil@tinyhumans.ai> Co-authored-by: M3gA-Mind <elvin@mahadao.com> Co-authored-by: oxoxDev <oxoxdev@users.noreply.github.com> Co-authored-by: mysma-9403 <64923976+mysma-9403@users.noreply.github.com> Co-authored-by: mwakidenis <mwakidenice@gmail.com> Co-authored-by: NgoQuocViet2001 <123613986+NgoQuocViet2001@users.noreply.github.com> Co-authored-by: viet.ngo <viet.ngo@sotatek.com> Co-authored-by: Maciej Myszkiewicz <mmyszkiewicz@bwcoders.com>
165 lines
5.9 KiB
Rust
165 lines
5.9 KiB
Rust
//! The typed dispatch helper every facade method is built on.
|
|
//!
|
|
//! # Why this goes through `invoke` rather than calling `ops::*` directly
|
|
//!
|
|
//! Calling a domain's `ops::*` function directly would be more strongly typed
|
|
//! and would skip a serde round-trip — but it would also **bypass
|
|
//! [`DomainSet`](crate::core::runtime::DomainSet) gating entirely**. Gating
|
|
//! works by not registering a domain's controllers at the single registration
|
|
//! site (`src/core/all.rs`); the `ops` functions themselves remain perfectly
|
|
//! callable. A facade built on `ops::*` would therefore happily serve a domain
|
|
//! the embedder explicitly switched off, and [`CoreError::Unavailable`] could
|
|
//! never be produced.
|
|
//!
|
|
//! Dispatch is also where [`CoreContext`](crate::core::runtime::CoreContext)
|
|
//! scoping already happens ([`CoreRuntime::invoke`] wraps the handler future in
|
|
//! `CoreContext::scope`), so routing through it means ambient workspace and
|
|
//! domain state are correct for free.
|
|
//!
|
|
//! The cost is a JSON round-trip and stringly-typed method names. Both are paid
|
|
//! *here*, once, so that no host ever sees them.
|
|
//!
|
|
//! # The response envelope
|
|
//!
|
|
//! Controller handlers serialize through
|
|
//! [`RpcOutcome::into_cli_compatible_json`](crate::rpc::RpcOutcome), which emits
|
|
//! a **variable shape**:
|
|
//!
|
|
//! - no logs → the value itself
|
|
//! - any logs → `{"result": <value>, "logs": [...]}`
|
|
//!
|
|
//! Whether a given method carries logs is an implementation detail of its
|
|
//! handler and can change without notice — `openhuman.config_get_runtime_flags`,
|
|
//! for instance, always emits exactly one log, so it is *always* wrapped. A
|
|
//! caller that deserialized the raw value would break on that method every
|
|
//! single time, and would break on others only after an unrelated edit added a
|
|
//! log line. [`unwrap_outcome_envelope`] absorbs that here so no facade method
|
|
//! and no host has to think about it.
|
|
|
|
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
|
|
use super::error::CoreError;
|
|
use crate::core::runtime::CoreRuntime;
|
|
|
|
/// Dispatch `method` with `params` and decode the result into `R`.
|
|
///
|
|
/// This is `pub(crate)`: it is the facade's own plumbing, not a host-facing
|
|
/// escape hatch. Hosts that genuinely need an unmodelled method use
|
|
/// [`Core::raw`](super::Core::raw) and own the JSON themselves.
|
|
pub(crate) async fn call<P, R>(
|
|
rt: &CoreRuntime,
|
|
method: &'static str,
|
|
params: P,
|
|
) -> Result<R, CoreError>
|
|
where
|
|
P: Serialize,
|
|
R: DeserializeOwned,
|
|
{
|
|
let params =
|
|
serde_json::to_value(params).map_err(|source| CoreError::Encode { method, source })?;
|
|
|
|
log::debug!("[embed] call method={method}");
|
|
|
|
let raw = rt
|
|
.invoke(method, params)
|
|
.await
|
|
.map_err(|e| CoreError::from_rpc_string(method, e))?;
|
|
|
|
let payload = unwrap_outcome_envelope(raw);
|
|
|
|
serde_json::from_value(payload).map_err(|source| {
|
|
log::debug!("[embed] decode_failed method={method} error={source}");
|
|
CoreError::Decode { method, source }
|
|
})
|
|
}
|
|
|
|
/// Strip the `{"result": …, "logs": […]}` wrapper when present.
|
|
///
|
|
/// The check is deliberately strict — exactly two keys, `logs` an array of
|
|
/// strings — so a domain type that merely *happens* to have a `result` field is
|
|
/// not mistaken for an envelope. That ambiguity is inherent to the wire format
|
|
/// (the envelope is untagged), and being conservative here means the failure
|
|
/// mode is a clean [`CoreError::Decode`] rather than silently handing back the
|
|
/// wrong object.
|
|
fn unwrap_outcome_envelope(value: serde_json::Value) -> serde_json::Value {
|
|
let serde_json::Value::Object(ref map) = value else {
|
|
return value;
|
|
};
|
|
|
|
if map.len() != 2 {
|
|
return value;
|
|
}
|
|
|
|
let Some(logs) = map.get("logs").and_then(|l| l.as_array()) else {
|
|
return value;
|
|
};
|
|
|
|
if !logs.iter().all(|entry| entry.is_string()) {
|
|
return value;
|
|
}
|
|
|
|
let Some(result) = map.get("result") else {
|
|
return value;
|
|
};
|
|
|
|
result.clone()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn unwraps_a_logged_outcome() {
|
|
let wire = json!({
|
|
"result": { "browser_allow_all": true, "log_prompts": false },
|
|
"logs": ["runtime flags read"]
|
|
});
|
|
assert_eq!(
|
|
unwrap_outcome_envelope(wire),
|
|
json!({ "browser_allow_all": true, "log_prompts": false })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn passes_through_an_unlogged_value() {
|
|
let wire = json!({ "browser_allow_all": true, "log_prompts": false });
|
|
assert_eq!(unwrap_outcome_envelope(wire.clone()), wire);
|
|
}
|
|
|
|
#[test]
|
|
fn passes_through_non_objects() {
|
|
assert_eq!(unwrap_outcome_envelope(json!(7)), json!(7));
|
|
assert_eq!(unwrap_outcome_envelope(json!(null)), json!(null));
|
|
assert_eq!(unwrap_outcome_envelope(json!([1, 2])), json!([1, 2]));
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_unwrap_a_domain_type_that_merely_has_a_result_field() {
|
|
// Three keys — not an envelope.
|
|
let wire = json!({ "result": "ok", "logs": ["a"], "extra": 1 });
|
|
assert_eq!(unwrap_outcome_envelope(wire.clone()), wire);
|
|
|
|
// `logs` is not an array of strings — not an envelope.
|
|
let wire = json!({ "result": "ok", "logs": 3 });
|
|
assert_eq!(unwrap_outcome_envelope(wire.clone()), wire);
|
|
|
|
let wire = json!({ "result": "ok", "logs": [{ "level": "info" }] });
|
|
assert_eq!(unwrap_outcome_envelope(wire.clone()), wire);
|
|
|
|
// Two keys but no `result` — not an envelope.
|
|
let wire = json!({ "value": "ok", "logs": ["a"] });
|
|
assert_eq!(unwrap_outcome_envelope(wire.clone()), wire);
|
|
}
|
|
|
|
#[test]
|
|
fn unwraps_an_empty_log_array_envelope() {
|
|
// Defensive: `into_cli_compatible_json` does not currently emit this
|
|
// (empty logs return the bare value), but the shape is unambiguous.
|
|
let wire = json!({ "result": { "a": 1 }, "logs": [] });
|
|
assert_eq!(unwrap_outcome_envelope(wire), json!({ "a": 1 }));
|
|
}
|
|
}
|