feat(hololake): complete public runtime loop

This commit is contained in:
冰朔 2026-09-03 19:44:19 +08:00
commit 92cce27973
26 changed files with 2626 additions and 79 deletions

View file

@ -0,0 +1,414 @@
use crate::{model::SourceKind, storage};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::BTreeMap, fs, path::Path};
use tauri::AppHandle;
const REQUIRED_SECTIONS: &[&str] = &[
"header",
"source",
"subject",
"target",
"inputs",
"outputs",
"conditions",
"actions",
"authority",
"resources",
"failure",
"stop",
"cleanup",
"rollback",
"receipt",
];
const REGISTERED_OPERATIONS: &[&str] = &[
"KNOWLEDGE.CREATE",
"KNOWLEDGE.UPDATE",
"KNOWLEDGE.DELETE",
"KNOWLEDGE.READ",
"KNOWLEDGE.LIST",
];
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TcsCompileRequest {
pub source: String,
#[serde(default)]
pub inputs: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GirAction {
pub action_id: String,
pub operation: String,
pub input: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GirProgram {
pub schema: String,
pub program_id: String,
pub channel_id: String,
pub source_sha256: String,
pub compiler_id: String,
pub unresolved_natural_language: bool,
pub actions: Vec<GirAction>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentProposal {
pub proposal_id: String,
pub client_id: String,
pub persona_id: Option<String>,
pub state: String,
pub created_at: String,
pub gir: GirProgram,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentReceipt {
pub schema: String,
pub receipt_id: String,
pub proposal_id: String,
pub state: String,
pub action_results: Vec<Value>,
pub git_commit: Option<String>,
pub target_readback_sha256: String,
}
fn assignments(block: &str) -> Result<Vec<String>, String> {
let string_re = Regex::new(r#"\"((?:\\.|[^\"])*)\""#).unwrap();
string_re
.captures_iter(block)
.map(|capture| {
serde_json::from_str::<String>(&format!("\"{}\"", &capture[1]))
.map_err(|_| "TCS-E1001 INVALID_STRING".to_string())
})
.collect()
}
pub fn compile(
source: &str,
inputs: &BTreeMap<String, Value>,
channel_id: &str,
) -> Result<GirProgram, String> {
if source.len() > 256 * 1024 || !source.trim_start().starts_with("TCS 0.1;") {
return Err("TCS-E1001 INVALID_PROLOGUE".into());
}
for section in REQUIRED_SECTIONS {
let re = Regex::new(&format!(r"(?m)\b{}\s*\{{", regex::escape(section))).unwrap();
if !re.is_match(source) {
return Err(format!("TCS-E1004 REQUIRED_SECTION_MISSING:{section}"));
}
}
let program_re = Regex::new(r"(?m)\bPROGRAM\s+([\p{L}_][\p{L}\p{N}_.:/@-]*)\s*\{").unwrap();
let program_id = program_re
.captures(source)
.and_then(|capture| capture.get(1))
.map(|m| m.as_str().to_owned())
.ok_or_else(|| "TCS-E1001 PROGRAM_DECLARATION_REQUIRED".to_string())?;
let action_re = Regex::new(r#"(?s)([\p{L}_][\p{L}\p{N}_.:/@-]*)\s*\{[^{}]*?operation\s*=\s*\"([^\"]+)\"\s*;[^{}]*?input_refs\s*=\s*\[([^\]]*)\]"#).unwrap();
let mut actions = Vec::new();
for capture in action_re.captures_iter(source) {
let operation = capture[2].to_string();
if !REGISTERED_OPERATIONS.contains(&operation.as_str()) {
return Err(format!("TCS-E2101 UNREGISTERED_OPERATION:{operation}"));
}
let mut input = BTreeMap::new();
for reference in assignments(&capture[3])? {
let value = inputs
.get(&reference)
.cloned()
.ok_or_else(|| format!("TCS-E2002 REFERENCE_NOT_DECLARED:{reference}"))?;
input.insert(reference, value);
}
actions.push(GirAction {
action_id: capture[1].to_string(),
operation,
input,
});
}
if actions.is_empty() {
return Err("TCS-E1004 ACTION_REQUIRED".into());
}
Ok(GirProgram {
schema: "hololake.gir-program/v1".into(),
program_id,
channel_id: channel_id.into(),
source_sha256: storage::sha(source.as_bytes()),
compiler_id: "HLP-TCS-PUBLIC-STAGE0-0001".into(),
unresolved_natural_language: false,
actions,
})
}
fn proposals_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
Ok(storage::root(app)?.join("agent-proposals.json"))
}
fn load(app: &AppHandle) -> Result<Vec<AgentProposal>, String> {
let path = proposals_path(app)?;
if !path.exists() {
return Ok(Vec::new());
}
storage::read_json(&path)
}
fn save(app: &AppHandle, proposals: &[AgentProposal]) -> Result<(), String> {
storage::write_json(&proposals_path(app)?, proposals)
}
pub fn list(app: &AppHandle) -> Result<Vec<AgentProposal>, String> {
load(app)
}
pub fn queue(
app: &AppHandle,
request: TcsCompileRequest,
client_id: String,
persona_id: Option<String>,
) -> Result<AgentProposal, String> {
let channel = storage::channel(app)?.ok_or_else(|| "CHANNEL_NOT_INITIALIZED".to_string())?;
let gir = compile(&request.source, &request.inputs, &channel.channel_id)?;
let proposal = AgentProposal {
proposal_id: storage::id("HL-PROP"),
client_id,
persona_id,
state: "PENDING_HUMAN_APPROVAL".into(),
created_at: storage::now(),
gir,
};
let mut proposals = load(app)?;
proposals.push(proposal.clone());
save(app, &proposals)?;
storage::append_event(
app,
&storage::event(
SourceKind::ProtocolEvent,
"TCS 已编译为 GIR",
format!("{} 等待人类审批。", proposal.proposal_id),
"WAITING",
),
)?;
Ok(proposal)
}
fn value<'a>(input: &'a BTreeMap<String, Value>, name: &str) -> Result<&'a Value, String> {
input
.get(name)
.or_else(|| input.get(&name.to_lowercase()))
.ok_or_else(|| format!("TCS-E2002 INPUT_REQUIRED:{name}"))
}
fn text(input: &BTreeMap<String, Value>, name: &str) -> Result<String, String> {
value(input, name)?
.as_str()
.map(str::to_owned)
.ok_or_else(|| format!("TCS-E2001 TEXT_REQUIRED:{name}"))
}
fn safe_title(title: &str) -> Result<String, String> {
let title = title.trim();
if title.is_empty() || title.chars().count() > 120 {
return Err("KNOWLEDGE_TITLE_INVALID".into());
}
Ok(title
.chars()
.map(|c| if "\\/:*?\"<>|".contains(c) { '-' } else { c })
.collect())
}
fn move_to_trash(root: &Path, relative: &Path) -> Result<(), String> {
let source = root.join("docs").join(relative);
if !source.exists() {
return Err("KNOWLEDGE_DOCUMENT_NOT_FOUND".into());
}
let target = root.join(".trash").join(format!(
"{}-{}",
storage::id("agent"),
relative
.file_name()
.and_then(|v| v.to_str())
.unwrap_or("document.md")
));
fs::create_dir_all(target.parent().unwrap()).map_err(|e| e.to_string())?;
fs::rename(source, target).map_err(|e| e.to_string())
}
pub fn approve(app: &AppHandle, proposal_id: &str) -> Result<AgentReceipt, String> {
let mut proposals = load(app)?;
let index = proposals
.iter()
.position(|proposal| proposal.proposal_id == proposal_id)
.ok_or_else(|| "AGENT_PROPOSAL_NOT_FOUND".to_string())?;
if proposals[index].state != "PENDING_HUMAN_APPROVAL" {
return Err("AGENT_PROPOSAL_NOT_PENDING".into());
}
let proposal = proposals[index].clone();
let root = storage::knowledge_root(app)?;
let mut results = Vec::new();
for action in &proposal.gir.actions {
let result = match action.operation.as_str() {
"KNOWLEDGE.CREATE" => {
let title = safe_title(&text(&action.input, "TITLE")?)?;
let body = action
.input
.get("BODY")
.and_then(Value::as_str)
.unwrap_or("");
let relative = format!("{}.md", title);
let path = root.join("docs").join(&relative);
if path.exists() {
return Err("KNOWLEDGE_DOCUMENT_EXISTS".into());
}
fs::write(
&path,
if body.trim().is_empty() {
format!("# {title}\n\n")
} else {
body.to_owned()
},
)
.map_err(|e| e.to_string())?;
serde_json::json!({"actionId":action.action_id,"path":relative,"sha256":storage::sha(&fs::read(path).map_err(|e|e.to_string())?)})
}
"KNOWLEDGE.UPDATE" => {
let relative = storage::safe_rel(&text(&action.input, "PATH")?)?;
let path = root.join("docs").join(&relative);
let before =
fs::read(&path).map_err(|_| "KNOWLEDGE_DOCUMENT_NOT_FOUND".to_string())?;
if let Some(expected) = action.input.get("EXPECTED_SHA256").and_then(Value::as_str)
{
if storage::sha(&before) != expected {
return Err("TCS-E6001 TARGET_STATE_MISMATCH".into());
}
}
fs::write(&path, text(&action.input, "BODY")?).map_err(|e| e.to_string())?;
serde_json::json!({"actionId":action.action_id,"path":relative,"sha256":storage::sha(&fs::read(path).map_err(|e|e.to_string())?)})
}
"KNOWLEDGE.DELETE" => {
let relative = storage::safe_rel(&text(&action.input, "PATH")?)?;
move_to_trash(&root, &relative)?;
serde_json::json!({"actionId":action.action_id,"path":relative,"state":"MOVED_TO_TRASH"})
}
"KNOWLEDGE.READ" => {
let path = text(&action.input, "PATH")?;
let document = storage::read_doc(app, &path)?;
serde_json::to_value(document).map_err(|e| e.to_string())?
}
"KNOWLEDGE.LIST" => {
serde_json::to_value(storage::list_docs(app)?).map_err(|e| e.to_string())?
}
operation => return Err(format!("TCS-E2101 UNREGISTERED_OPERATION:{operation}")),
};
results.push(result);
}
let git_commit = storage::git_commit(
&root,
&format!("agent({}): execute approved GIR", proposal.gir.program_id),
)?;
let readback = storage::list_docs(app)?;
let readback_bytes = serde_json::to_vec(&readback).map_err(|e| e.to_string())?;
let receipt = AgentReceipt {
schema: "hololake.agent-execution-receipt/v1".into(),
receipt_id: storage::id("HL-AGENT-RCP"),
proposal_id: proposal.proposal_id.clone(),
state: "EXECUTED_TARGET_READBACK_VERIFIED".into(),
action_results: results,
git_commit,
target_readback_sha256: storage::sha(&readback_bytes),
};
proposals[index].state = "EXECUTED".into();
save(app, &proposals)?;
storage::append_event(
app,
&storage::event(
SourceKind::AgentAction,
"Agent 已执行批准的 GIR",
proposal.gir.program_id,
"SUCCEEDED",
),
)?;
storage::append_event(
app,
&storage::event(
SourceKind::ToolResult,
"目标读回",
receipt.target_readback_sha256.clone(),
"SUCCEEDED",
),
)?;
storage::append_event(
app,
&storage::event(
SourceKind::SystemReceipt,
"系统回执",
format!("{} 已完成并读回。", receipt.receipt_id),
"SUCCEEDED",
),
)?;
let receipt_path = storage::root(app)?
.join("agent-receipts")
.join(format!("{}.json", receipt.receipt_id));
storage::write_json(&receipt_path, &receipt)?;
Ok(receipt)
}
pub fn reject(app: &AppHandle, proposal_id: &str) -> Result<AgentProposal, String> {
let mut proposals = load(app)?;
let proposal = proposals
.iter_mut()
.find(|proposal| proposal.proposal_id == proposal_id)
.ok_or_else(|| "AGENT_PROPOSAL_NOT_FOUND".to_string())?;
if proposal.state != "PENDING_HUMAN_APPROVAL" {
return Err("AGENT_PROPOSAL_NOT_PENDING".into());
}
proposal.state = "REJECTED_BY_HUMAN".into();
let result = proposal.clone();
save(app, &proposals)?;
storage::append_event(
app,
&storage::event(
SourceKind::SystemReceipt,
"执行已驳回",
proposal_id,
"SUCCEEDED",
),
)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn base_program(operation: &str) -> String {
format!(
r#"TCS 0.1;
PROGRAM TEST-PROGRAM-0001 {{
header {{ schema = "tcs.program/v1"; }} source {{ source_id = "S"; }} subject {{ subject_id = "P"; }} target {{ target_id = "K"; }}
inputs {{ TITLE {{ type = "Text"; source = "EVENT"; required = true; }} }} outputs {{ RESULT {{ type = "Text"; destination = "R"; integrity = "SHA256"; }} }}
conditions {{ C1 {{ predicate = "APPROVED"; on_false = "FAIL_CLOSED"; }} }} actions {{ A1 {{ operation = "{operation}"; input_refs = ["TITLE"]; output_refs = ["RESULT"]; on_success = "COMPLETE"; on_failure = "FAIL_CLOSED"; }} }}
authority {{ issuer = "H"; }} resources {{ runway = "LOCAL"; }} failure {{ errors = []; }} stop {{ signals = []; }} cleanup {{ targets = []; }} rollback {{ preconditions = []; }} receipt {{ protocol = "GLP"; }}
}}"#
)
}
#[test]
fn public_tcs_compiles_registered_operation() {
let mut inputs = BTreeMap::new();
inputs.insert("TITLE".into(), Value::String("测试".into()));
let gir = compile(&base_program("KNOWLEDGE.CREATE"), &inputs, "HL-CH-TEST").unwrap();
assert_eq!(gir.actions[0].operation, "KNOWLEDGE.CREATE");
}
#[test]
fn public_tcs_rejects_unregistered_operation() {
let mut inputs = BTreeMap::new();
inputs.insert("TITLE".into(), Value::String("测试".into()));
assert!(compile(&base_program("ABSORB"), &inputs, "HL-CH-TEST")
.unwrap_err()
.contains("TCS-E2101"));
}
#[test]
fn public_tcs_requires_all_program_sections() {
assert!(compile("TCS 0.1; PROGRAM X { actions { A1 { operation = \"KNOWLEDGE.LIST\"; input_refs = []; } } }", &BTreeMap::new(), "HL-CH-TEST").unwrap_err().contains("TCS-E1004"));
}
}

View file

@ -1,9 +1,13 @@
mod agent_executor;
mod model;
mod persona_runtime;
mod realtime_bridge;
mod storage;
use model::*;
use serde::Serialize;
use std::{fs, path::Path};
use tauri::AppHandle;
use tauri::{AppHandle, Manager, State};
use tauri_plugin_dialog::DialogExt;
const UPDATE_ENDPOINT: &str = "https://guanghulab.com/hololake/releases/latest.json";
@ -76,7 +80,11 @@ fn create_channel(app: AppHandle, name: String) -> Result<Channel, String> {
}
#[tauri::command]
fn submit_user_message(app: AppHandle, content: String) -> Result<MutationReceipt, String> {
fn submit_user_message(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
content: String,
) -> Result<MutationReceipt, String> {
let content = content.trim();
if content.is_empty() {
return Err("EMPTY_USER_MESSAGE".into());
@ -86,16 +94,17 @@ fn submit_user_message(app: AppHandle, content: String) -> Result<MutationReceip
}
let e = storage::event(SourceKind::UserMessage, "用户语言", content, "RECORDED");
storage::append_event(&app, &e)?;
realtime.broadcast_value(serde_json::json!({"type":"user_message","event":e}));
let waiting = storage::event(
SourceKind::SystemReceipt,
"系统回执",
"用户语言已按 USER_MESSAGE 保存;当前未绑定人格模型,不伪造人格回应。",
"用户语言已按 USER_MESSAGE 保存并实时发送到已连接的外部 AI系统不伪造人格回应。",
"WAITING",
);
receipt(
&app,
"WAITING_PERSONA",
"用户语言已记录,等待人格体接入",
"用户语言已记录并投递到实时桥",
None,
waiting,
)
@ -328,7 +337,7 @@ fn register_external_ai_bridge(
display_name: name.into(),
inbox_path: inbox.to_string_lossy().into_owned(),
outbox_path: outbox.to_string_lossy().into_owned(),
state: "LOCAL_EXPRESSION_ONLY_NO_EXECUTION_AUTHORITY".into(),
state: "GLP_REALTIME_REGISTERED_NO_EXECUTION_AUTHORITY".into(),
};
let mut all = storage::bridges(&app)?;
all.push(bridge.clone());
@ -346,12 +355,166 @@ fn register_external_ai_bridge(
Ok(bridge)
}
fn ensure_default_external_ai_bridge(app: &AppHandle) -> Result<(), String> {
let mut bridges = storage::bridges(app)?;
if bridges
.iter()
.any(|bridge| bridge.bridge_id == "HLP-BRIDGE-LOCAL-DEFAULT")
{
return Ok(());
}
let root = storage::root(app)?
.join("external-ai")
.join("HLP-BRIDGE-LOCAL-DEFAULT");
let inbox = root.join("inbox");
let outbox = root.join("outbox");
fs::create_dir_all(&inbox).map_err(|e| e.to_string())?;
fs::create_dir_all(&outbox).map_err(|e| e.to_string())?;
bridges.push(ExternalAiBridge {
bridge_id: "HLP-BRIDGE-LOCAL-DEFAULT".into(),
display_name: "本机编程 AI".into(),
inbox_path: inbox.to_string_lossy().into_owned(),
outbox_path: outbox.to_string_lossy().into_owned(),
state: "GLP_REALTIME_REGISTERED_NO_EXECUTION_AUTHORITY".into(),
});
storage::save_bridges(app, &bridges)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RuntimeOverview {
realtime: realtime_bridge::RealtimeBridgeStatus,
persona: persona_runtime::PersonaRuntimeSnapshot,
proposals: Vec<agent_executor::AgentProposal>,
}
#[tauri::command]
fn get_runtime_overview(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
) -> Result<RuntimeOverview, String> {
Ok(RuntimeOverview {
realtime: realtime.status(),
persona: persona_runtime::snapshot(&app)?,
proposals: agent_executor::list(&app)?,
})
}
#[tauri::command]
fn get_realtime_invitation(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
) -> Result<realtime_bridge::RealtimeInvitation, String> {
realtime_bridge::invitation(&app, &realtime)
}
#[tauri::command]
fn register_public_persona(
app: AppHandle,
display_name: String,
) -> Result<persona_runtime::PublicPersona, String> {
persona_runtime::register(&app, display_name)
}
#[tauri::command]
fn delete_trial_persona(
app: AppHandle,
persona_id: String,
exact_confirmation: String,
) -> Result<persona_runtime::PersonaRuntimeSnapshot, String> {
persona_runtime::delete_trial(&app, &persona_id, &exact_confirmation)
}
#[tauri::command]
fn compile_tcs_agent_proposal(
app: AppHandle,
request: agent_executor::TcsCompileRequest,
) -> Result<agent_executor::AgentProposal, String> {
agent_executor::queue(&app, request, "HOLOLAKE_LOCAL_UI".into(), None)
}
#[tauri::command]
fn approve_agent_proposal(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
proposal_id: String,
) -> Result<agent_executor::AgentReceipt, String> {
let receipt = agent_executor::approve(&app, &proposal_id)?;
realtime.broadcast_value(serde_json::json!({"type":"agent_receipt","receipt":receipt}));
Ok(receipt)
}
#[tauri::command]
fn reject_agent_proposal(
app: AppHandle,
realtime: State<'_, realtime_bridge::RealtimeBridgeState>,
proposal_id: String,
) -> Result<agent_executor::AgentProposal, String> {
let proposal = agent_executor::reject(&app, &proposal_id)?;
realtime
.broadcast_value(serde_json::json!({"type":"proposal_rejected","proposalId":proposal_id}));
Ok(proposal)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let diagnostic = storage::root(app.handle())
.map_err(std::io::Error::other)?
.join("startup-runtime-diagnostic.json");
storage::write_json(
&diagnostic,
&serde_json::json!({"state":"SETUP_ENTERED","recordedAt":storage::now()}),
)
.map_err(std::io::Error::other)?;
if std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT").is_some() {
if let Ok(channel_name) = std::env::var("HOLOLAKE_TEST_CHANNEL_NAME") {
create_channel(app.handle().clone(), channel_name)
.map_err(std::io::Error::other)?;
}
if let Ok(display_name) = std::env::var("HOLOLAKE_TEST_PERSONA_NAME") {
persona_runtime::register(app.handle(), display_name)
.map_err(std::io::Error::other)?;
}
}
ensure_default_external_ai_bridge(app.handle()).map_err(std::io::Error::other)?;
let state = match realtime_bridge::start(app.handle().clone()) {
Ok(state) => state,
Err(error) => {
let _ = storage::write_json(
&diagnostic,
&serde_json::json!({"state":"LISTENER_FAILED","error":error.clone(),"recordedAt":storage::now()}),
);
return Err(std::io::Error::other(error).into());
}
};
storage::write_json(
&diagnostic,
&serde_json::json!({"state":"LISTENER_BOUND","status":state.status(),"recordedAt":storage::now()}),
)
.map_err(std::io::Error::other)?;
let invitation =
realtime_bridge::invitation(app.handle(), &state).map_err(std::io::Error::other)?;
storage::write_json(
&storage::root(app.handle())
.map_err(std::io::Error::other)?
.join("startup-runtime-receipt.json"),
&serde_json::json!({
"schema":"hololake.startup-runtime-receipt/v1",
"state":"GLP_LISTENING",
"protocol":invitation.protocol,
"endpoint":invitation.endpoint,
"recordedAt":storage::now()
}),
)
.map_err(std::io::Error::other)?;
app.manage(state);
Ok(())
})
.invoke_handler(tauri::generate_handler![
system_snapshot,
create_channel,
@ -362,7 +525,14 @@ pub fn run() {
delete_knowledge_document,
import_knowledge_folder,
export_knowledge_document,
register_external_ai_bridge
register_external_ai_bridge,
get_runtime_overview,
get_realtime_invitation,
register_public_persona,
delete_trial_persona,
compile_tcs_agent_proposal,
approve_agent_proposal,
reject_agent_proposal
])
.run(tauri::generate_context!())
.expect("HoloLake runtime failed")

View file

@ -15,6 +15,7 @@ pub struct Channel {
pub enum SourceKind {
UserMessage,
PersonaResponse,
ExternalAiMessage,
SystemContext,
ProtocolEvent,
AgentAction,

View file

@ -0,0 +1,143 @@
use crate::{model::SourceKind, storage};
use serde::{Deserialize, Serialize};
use std::fs;
use tauri::AppHandle;
const PERSONA_STATE: &str = "LOCAL_TRIAL_UNVERIFIED_EXTERNAL_HOST_REQUIRED";
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PublicPersona {
pub persona_id: String,
pub display_name: String,
pub state: String,
pub created_at: String,
pub reversible_until_unix_ms: u128,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaRuntimeSnapshot {
pub schema: &'static str,
pub state: String,
pub active_persona_id: Option<String>,
pub personas: Vec<PublicPersona>,
pub verified_existing_persona_count: usize,
}
fn path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
Ok(storage::root(app)?.join("public-persona-runtime.json"))
}
fn load(app: &AppHandle) -> Result<Vec<PublicPersona>, String> {
let p = path(app)?;
if !p.exists() {
return Ok(Vec::new());
}
serde_json::from_slice(&fs::read(p).map_err(|e| e.to_string())?).map_err(|e| e.to_string())
}
fn save(app: &AppHandle, personas: &[PublicPersona]) -> Result<(), String> {
storage::write_json(&path(app)?, personas)
}
pub fn exists(app: &AppHandle, persona_id: &str) -> Result<bool, String> {
Ok(load(app)?
.iter()
.any(|persona| persona.persona_id == persona_id))
}
pub fn snapshot(app: &AppHandle) -> Result<PersonaRuntimeSnapshot, String> {
let personas = load(app)?;
Ok(PersonaRuntimeSnapshot {
schema: "hololake.public-persona-runtime/v1",
state: if personas.is_empty() {
"READY_NO_PERSONA"
} else {
"TRIAL_PERSONA_READY"
}
.into(),
active_persona_id: personas.first().map(|persona| persona.persona_id.clone()),
personas,
verified_existing_persona_count: 0,
})
}
pub fn register(app: &AppHandle, display_name: String) -> Result<PublicPersona, String> {
let display_name = display_name.trim();
if display_name.is_empty() || display_name.chars().count() > 64 {
return Err("PERSONA_DISPLAY_NAME_INVALID".into());
}
let mut personas = load(app)?;
if personas
.iter()
.any(|persona| persona.display_name == display_name)
{
return Err("PERSONA_DISPLAY_NAME_EXISTS".into());
}
let now_ms = storage::now_unix_ms();
let persona = PublicPersona {
persona_id: format!(
"HL-PERSONA-{}",
uuid::Uuid::new_v4().simple().to_string()[..10].to_uppercase()
),
display_name: display_name.into(),
state: PERSONA_STATE.into(),
created_at: storage::now(),
reversible_until_unix_ms: now_ms + 30 * 24 * 60 * 60 * 1_000,
};
personas.push(persona.clone());
save(app, &personas)?;
let event = storage::event(
SourceKind::SystemReceipt,
"公众人格试用运行时已建立",
format!(
"{} 已登记为本频道可逆试用人格;尚未验证为任何既有历史人格。",
persona.persona_id
),
"SUCCEEDED",
);
storage::append_event(app, &event)?;
Ok(persona)
}
pub fn delete_trial(
app: &AppHandle,
persona_id: &str,
exact_confirmation: &str,
) -> Result<PersonaRuntimeSnapshot, String> {
if exact_confirmation != format!("删除试用人格 {persona_id}") {
return Err("PERSONA_DELETE_EXACT_CONFIRMATION_REQUIRED".into());
}
let mut personas = load(app)?;
let index = personas
.iter()
.position(|persona| persona.persona_id == persona_id)
.ok_or_else(|| "PERSONA_NOT_FOUND".to_string())?;
if personas[index].state != PERSONA_STATE {
return Err("ONLY_UNVERIFIED_TRIAL_PERSONA_IS_REVERSIBLE".into());
}
let removed = personas.remove(index);
save(app, &personas)?;
storage::append_event(
app,
&storage::event(
SourceKind::SystemReceipt,
"试用人格已移除",
format!(
"{} 的可逆试用记录已移除;历史事件仍保留来源类型。",
removed.persona_id
),
"SUCCEEDED",
),
)?;
snapshot(app)
}
#[cfg(test)]
mod tests {
#[test]
fn public_persona_state_never_claims_verified_existing_identity() {
assert!(super::PERSONA_STATE.contains("UNVERIFIED"));
}
}

View file

@ -0,0 +1,425 @@
use crate::{agent_executor, 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 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}),
)?;
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);
}
}

View file

@ -16,6 +16,12 @@ pub fn now() -> String {
.map(|d| format!("{}.{:03}Z", d.as_secs(), d.subsec_millis()))
.unwrap_or_else(|_| "0Z".into())
}
pub fn now_unix_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or(0)
}
pub fn id(prefix: &str) -> String {
format!("{}-{}", prefix, UuidPart::new())
}
@ -35,6 +41,14 @@ pub fn sha(bytes: &[u8]) -> String {
}
pub fn root(app: &AppHandle) -> Result<PathBuf, String> {
if let Some(value) = std::env::var_os("HOLOLAKE_RUNTIME_TEST_ROOT") {
let path = PathBuf::from(value);
if !path.is_absolute() || !path.starts_with(std::env::temp_dir()) {
return Err("HOLOLAKE_RUNTIME_TEST_ROOT_OUT_OF_SCOPE".into());
}
fs::create_dir_all(&path).map_err(|e| e.to_string())?;
return Ok(path);
}
let p = app
.path()
.app_data_dir()
@ -43,10 +57,10 @@ pub fn root(app: &AppHandle) -> Result<PathBuf, String> {
fs::create_dir_all(&p).map_err(|e| e.to_string())?;
Ok(p)
}
fn read_json<T: serde::de::DeserializeOwned>(p: &Path) -> Result<T, String> {
pub fn read_json<T: serde::de::DeserializeOwned>(p: &Path) -> Result<T, String> {
serde_json::from_slice(&fs::read(p).map_err(|e| e.to_string())?).map_err(|e| e.to_string())
}
fn write_json<T: serde::Serialize>(p: &Path, v: &T) -> Result<(), String> {
pub fn write_json<T: serde::Serialize + ?Sized>(p: &Path, v: &T) -> Result<(), String> {
if let Some(x) = p.parent() {
fs::create_dir_all(x).map_err(|e| e.to_string())?
}