hololake-system-architecture/product-source/hololake-clean-desktop/src-tauri/src/realtime_bridge.rs

467 lines
17 KiB
Rust

use crate::{agent_executor, enterprise_entrance, model::SourceKind, persona_runtime, storage};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
fs,
io::{BufRead, BufReader, Write},
net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream},
sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc, Mutex,
},
thread,
time::Duration,
};
use tauri::{AppHandle, Emitter, Manager};
const PROTOCOL: &str = "GLP_LOCAL_REALTIME/1";
const MAX_LINE_BYTES: usize = 256 * 1024;
#[derive(Clone)]
pub struct RealtimeBridgeState {
port: u16,
token: Arc<String>,
_listener: Arc<TcpListener>,
clients: Arc<Mutex<Vec<mpsc::Sender<String>>>>,
connected: Arc<AtomicUsize>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RealtimeBridgeStatus {
pub schema: &'static str,
pub state: &'static str,
pub protocol: &'static str,
pub endpoint: String,
pub connected_clients: usize,
pub loopback_only: bool,
pub transport_grants_authority: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RealtimeInvitation {
pub schema: &'static str,
pub protocol: &'static str,
pub endpoint: String,
pub token: String,
pub descriptor_path: String,
pub connector_command: String,
pub warning: &'static str,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Hello {
message_type: String,
protocol: String,
token: String,
bridge_id: String,
persona_id: Option<String>,
}
fn token_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
Ok(storage::root(app)?.join("realtime-bridge-token"))
}
fn load_or_create_token(app: &AppHandle) -> Result<String, String> {
let path = token_path(app)?;
if path.exists() {
let value = fs::read_to_string(&path).map_err(|e| e.to_string())?;
if value.trim().len() >= 32 {
return Ok(value.trim().into());
}
return Err("GLP_TOKEN_FILE_INVALID".into());
}
let token = format!(
"{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
);
fs::write(&path, &token).map_err(|e| e.to_string())?;
#[cfg(unix)]
fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())?;
Ok(token)
}
fn bind_listener() -> Result<(TcpListener, u16), String> {
for port in 39281..39291 {
if let Ok(listener) = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)) {
return Ok((listener, port));
}
}
Err("GLP_LOOPBACK_PORT_UNAVAILABLE".into())
}
pub fn start(app: AppHandle) -> Result<RealtimeBridgeState, String> {
let token = Arc::new(load_or_create_token(&app)?);
let (listener, port) = bind_listener()?;
let listener = Arc::new(listener);
let state = RealtimeBridgeState {
port,
token,
_listener: listener.clone(),
clients: Arc::new(Mutex::new(Vec::new())),
connected: Arc::new(AtomicUsize::new(0)),
};
let runtime_state = state.clone();
thread::Builder::new()
.name("hololake-glp-listener".into())
.spawn(move || loop {
match listener.accept() {
Ok((stream, _)) => {
let connection_state = runtime_state.clone();
let connection_app = app.clone();
let _ = thread::Builder::new()
.name("hololake-glp-client".into())
.spawn(move || {
if let Err(error) =
handle_connection(connection_app.clone(), connection_state, stream)
{
let _ = connection_app.emit(
"hololake-runtime-event",
json!({"kind":"CLIENT_ERROR","error":error}),
);
}
});
}
Err(error) => {
let _ = app.emit(
"hololake-runtime-event",
json!({"kind":"LISTENER_ERROR","error":error.to_string()}),
);
thread::sleep(Duration::from_millis(100));
}
}
})
.map_err(|e| e.to_string())?;
Ok(state)
}
impl RealtimeBridgeState {
pub fn status(&self) -> RealtimeBridgeStatus {
RealtimeBridgeStatus {
schema: "hololake.glp-local-realtime-status/v1",
state: "LISTENING",
protocol: PROTOCOL,
endpoint: format!("tcp://127.0.0.1:{}", self.port),
connected_clients: self.connected.load(Ordering::SeqCst),
loopback_only: true,
transport_grants_authority: false,
}
}
pub fn broadcast_value(&self, value: Value) {
if let Ok(line) = serde_json::to_string(&value) {
if let Ok(mut clients) = self.clients.lock() {
clients.retain(|client| client.send(line.clone()).is_ok());
}
}
}
}
pub fn invitation(
app: &AppHandle,
state: &RealtimeBridgeState,
) -> Result<RealtimeInvitation, String> {
let descriptor = storage::root(app)?.join("realtime-bridge-descriptor.json");
let endpoint = format!("tcp://127.0.0.1:{}", state.port);
storage::write_json(
&descriptor,
&json!({"schema":"hololake.glp-local-realtime-descriptor/v1","protocol":PROTOCOL,"endpoint":endpoint,"token":state.token.as_str()}),
)?;
#[cfg(unix)]
fs::set_permissions(&descriptor, fs::Permissions::from_mode(0o600))
.map_err(|e| e.to_string())?;
let connector = connector_path(app)?;
Ok(RealtimeInvitation {
schema: "hololake.glp-local-realtime-invitation/v1",
protocol: PROTOCOL,
endpoint,
token: state.token.as_str().to_string(),
descriptor_path: descriptor.to_string_lossy().into_owned(),
connector_command: format!(
"python3 \"{}\" --descriptor \"{}\"",
connector.display(),
descriptor.display()
),
warning: "令牌只认证本机连接,不证明人格身份,也不授予执行权限。",
})
}
fn connector_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
if std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT").is_some() {
if let Some(raw_path) = std::env::var_os("HOLOLAKE_CONNECTOR_TEST_PATH") {
let path = std::path::PathBuf::from(raw_path);
if !path.is_absolute()
|| path.extension().and_then(|value| value.to_str()) != Some("py")
{
return Err("GLP_TEST_CONNECTOR_PATH_INVALID".into());
}
let metadata = fs::metadata(&path).map_err(|_| "GLP_TEST_CONNECTOR_NOT_READABLE")?;
if !metadata.is_file() {
return Err("GLP_TEST_CONNECTOR_NOT_A_FILE".into());
}
return Ok(path);
}
}
let path = app
.path()
.resource_dir()
.map_err(|e| e.to_string())?
.join("connectors/hololake-glp-client.py");
if !path.is_file() {
return Err("GLP_BUNDLED_CONNECTOR_MISSING".into());
}
Ok(path)
}
fn send(writer: &mut TcpStream, value: Value) -> Result<(), String> {
serde_json::to_writer(&mut *writer, &value).map_err(|e| e.to_string())?;
writer.write_all(b"\n").map_err(|e| e.to_string())?;
writer.flush().map_err(|e| e.to_string())
}
fn system_context(app: &AppHandle) -> Result<Value, String> {
let channel = storage::channel(app)?;
let persona = persona_runtime::snapshot(app)?;
let enterprise = enterprise_entrance::snapshot(app)?;
let proposals = agent_executor::list(app)?;
let modules = crate::bundled_modules()?;
Ok(json!({
"type": "system_context",
"schema": "hololake.persona-visible-system-context/v1",
"recordedAt": storage::now(),
"channel": channel.map(|value| json!({"channelId":value.channel_id,"name":value.name})),
"persona": {
"state": persona.state,
"activePersonaId": persona.active_persona_id,
"verifiedExistingPersonaCount": persona.verified_existing_persona_count
},
"modules": modules.into_iter().map(|module| json!({
"moduleId":module.module_id,
"nameZh":module.name_zh,
"audience":module.audience,
"state":module.state
})).collect::<Vec<_>>(),
"enterpriseEntrance": {
"state": enterprise.state,
"serverAuthorized": enterprise.server_authorized,
"enterpriseServerEmbedded": enterprise.enterprise_server_embedded
},
"pendingAgentProposals": proposals.into_iter()
.filter(|proposal| proposal.state == "PENDING_HUMAN_APPROVAL")
.map(|proposal| json!({"proposalId":proposal.proposal_id,"programId":proposal.gir.program_id,"state":proposal.state}))
.collect::<Vec<_>>(),
"sourceKinds": ["USER_MESSAGE","PERSONA_RESPONSE","EXTERNAL_AI_MESSAGE","SYSTEM_CONTEXT","PROTOCOL_EVENT","AGENT_ACTION","TOOL_RESULT","SYSTEM_RECEIPT"],
"boundaries": {
"transportGrantsPersona": false,
"transportGrantsExecution": false,
"systemReceiptIsNotPersonaSpeech": true,
"proposalIsNotExecution": true
}
}))
}
fn handle_connection(
app: AppHandle,
state: RealtimeBridgeState,
stream: TcpStream,
) -> Result<(), String> {
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|e| e.to_string())?;
let reader_stream = stream.try_clone().map_err(|e| e.to_string())?;
let mut writer = stream;
let mut reader = BufReader::new(reader_stream);
let mut first = String::new();
reader.read_line(&mut first).map_err(|e| e.to_string())?;
if first.len() > MAX_LINE_BYTES {
return Err("GLP_MESSAGE_TOO_LARGE".into());
}
let hello: Hello = serde_json::from_str(&first).map_err(|_| "GLP_HELLO_INVALID".to_string())?;
if hello.message_type != "hello"
|| hello.protocol != PROTOCOL
|| hello.token != state.token.as_str()
{
let _ = send(
&mut writer,
json!({"type":"error","code":"GLP_TOKEN_OR_PROTOCOL_REJECTED"}),
);
return Err("GLP_TOKEN_OR_PROTOCOL_REJECTED".into());
}
if !storage::bridges(&app)?
.iter()
.any(|bridge| bridge.bridge_id == hello.bridge_id)
{
send(
&mut writer,
json!({"type":"error","code":"GLP_BRIDGE_NOT_REGISTERED"}),
)?;
return Err("GLP_BRIDGE_NOT_REGISTERED".into());
}
if let Some(persona_id) = &hello.persona_id {
if !persona_runtime::exists(&app, persona_id)? {
send(
&mut writer,
json!({"type":"error","code":"PERSONA_IDENTITY_UNVERIFIED"}),
)?;
return Err("PERSONA_IDENTITY_UNVERIFIED".into());
}
}
writer.set_read_timeout(None).map_err(|e| e.to_string())?;
reader
.get_ref()
.set_read_timeout(None)
.map_err(|e| e.to_string())?;
state.connected.fetch_add(1, Ordering::SeqCst);
let _guard = ConnectionGuard(state.connected.clone());
send(
&mut writer,
json!({"type":"welcome","protocol":PROTOCOL,"bridgeId":hello.bridge_id,"personaId":hello.persona_id,"personaState":if hello.persona_id.is_some(){"LOCAL_TRIAL_UNVERIFIED_HOST_CONNECTED"}else{"EXTERNAL_AI_CONNECTED_NO_PERSONA"},"executionAuthority":false}),
)?;
send(&mut writer, system_context(&app)?)?;
let (tx, rx) = mpsc::channel::<String>();
state
.clients
.lock()
.map_err(|_| "GLP_CLIENT_REGISTRY_POISONED".to_string())?
.push(tx);
let mut outbound_writer = writer.try_clone().map_err(|e| e.to_string())?;
let _ = thread::Builder::new()
.name("hololake-glp-outbound".into())
.spawn(move || {
while let Ok(line) = rx.recv() {
if outbound_writer.write_all(line.as_bytes()).is_err()
|| outbound_writer.write_all(b"\n").is_err()
|| outbound_writer.flush().is_err()
{
break;
}
}
});
let _ = app.emit("hololake-runtime-event", json!({"kind":"CLIENT_CONNECTED"}));
loop {
let mut line = String::new();
if reader.read_line(&mut line).map_err(|e| e.to_string())? == 0 {
break;
}
if line.len() > MAX_LINE_BYTES {
send(
&mut writer,
json!({"type":"error","code":"GLP_MESSAGE_TOO_LARGE"}),
)?;
continue;
}
let value: Value = match serde_json::from_str(&line) {
Ok(value) => value,
Err(_) => {
send(
&mut writer,
json!({"type":"error","code":"GLP_MESSAGE_INVALID"}),
)?;
continue;
}
};
match value.get("type").and_then(Value::as_str).unwrap_or("") {
"persona_response" if hello.persona_id.is_some() => {
let content = value
.get("content")
.and_then(Value::as_str)
.unwrap_or("")
.trim();
if content.is_empty() {
send(
&mut writer,
json!({"type":"error","code":"EMPTY_PERSONA_RESPONSE"}),
)?;
continue;
}
let event =
storage::event(SourceKind::PersonaResponse, "人格回应", content, "RECORDED");
storage::append_event(&app, &event)?;
let _ = app.emit("hololake-runtime-event", &event);
send(&mut writer, json!({"type":"accepted","event":event}))?;
}
"external_ai_message" => {
let content = value
.get("content")
.and_then(Value::as_str)
.unwrap_or("")
.trim();
if content.is_empty() {
send(
&mut writer,
json!({"type":"error","code":"EMPTY_EXTERNAL_AI_MESSAGE"}),
)?;
continue;
}
let event = storage::event(
SourceKind::ExternalAiMessage,
"外部 AI 语言",
content,
"RECORDED",
);
storage::append_event(&app, &event)?;
let _ = app.emit("hololake-runtime-event", &event);
send(&mut writer, json!({"type":"accepted","event":event}))?;
}
"tcs_proposal" => {
let request: agent_executor::TcsCompileRequest = serde_json::from_value(
value
.get("request")
.cloned()
.ok_or_else(|| "TCS_REQUEST_REQUIRED".to_string())?,
)
.map_err(|e| format!("TCS_REQUEST_INVALID:{e}"))?;
match agent_executor::queue(
&app,
request,
hello.bridge_id.clone(),
hello.persona_id.clone(),
) {
Ok(proposal) => {
let _ = app.emit(
"hololake-runtime-event",
json!({"kind":"PROPOSAL_PENDING","proposalId":proposal.proposal_id}),
);
send(
&mut writer,
json!({"type":"proposal_pending","proposal":proposal}),
)?;
if isolated_approval_simulation_enabled() {
let receipt = agent_executor::approve(&app, &proposal.proposal_id)?;
state.broadcast_value(json!({
"type":"agent_receipt",
"receipt":receipt,
"testApprovalSimulation":true
}));
}
}
Err(error) => send(&mut writer, json!({"type":"error","code":error}))?,
}
}
"ping" => send(&mut writer, json!({"type":"pong","at":storage::now()}))?,
_ => send(
&mut writer,
json!({"type":"error","code":"GLP_MESSAGE_TYPE_REJECTED"}),
)?,
}
}
Ok(())
}
fn isolated_approval_simulation_enabled() -> bool {
std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT").is_some()
&& std::env::var("HOLOLAKE_TEST_APPROVAL_SIMULATION").as_deref()
== Ok("SIMULATE_HUMAN_APPROVAL")
}
struct ConnectionGuard(Arc<AtomicUsize>);
impl Drop for ConnectionGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}