feat(hololake): 接入铸渊自我看见与本地开发桥
This commit is contained in:
parent
c6af17957c
commit
5c86f85242
17 changed files with 986 additions and 6 deletions
|
|
@ -1335,6 +1335,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||
name = "hololake-native-desktop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
|
@ -1343,6 +1344,7 @@ dependencies = [
|
|||
"tauri-runtime",
|
||||
"tauri-runtime-wry",
|
||||
"tempfile",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
tauri-build = { version = "2.5.4", features = [] }
|
||||
|
||||
[dependencies]
|
||||
ring = "0.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "=2.10.2", features = [] }
|
||||
tauri-runtime = "=2.10.0"
|
||||
tauri-runtime-wry = "=2.10.0"
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
mod local_development_bridge;
|
||||
mod release_trust;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
local_development_bridge::acquire_development_write_lane,
|
||||
local_development_bridge::inspect_development_write_lane,
|
||||
local_development_bridge::release_development_write_lane,
|
||||
])
|
||||
.setup(|app| {
|
||||
release_trust::install_updater_if_provisioned(app.handle())?;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,426 @@
|
|||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use uuid::Uuid;
|
||||
|
||||
const BRIDGE_SCHEMA: &str = "hololake.local-development-writer/v1";
|
||||
const MAX_ID_BYTES: usize = 128;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AcquireWriteLaneInput {
|
||||
pub account_id: String,
|
||||
pub lane_id: String,
|
||||
pub owner_instance_id: String,
|
||||
pub resume_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectWriteLaneInput {
|
||||
pub account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReleaseWriteLaneInput {
|
||||
pub account_id: String,
|
||||
pub lane_id: String,
|
||||
pub owner_instance_id: String,
|
||||
pub resume_token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WriteLaneReceipt {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub account_key: String,
|
||||
pub lane_id: String,
|
||||
pub owner_instance_id: String,
|
||||
pub acquired_at_unix_ms: u128,
|
||||
pub observed_at_unix_ms: u128,
|
||||
pub resume_token: Option<String>,
|
||||
pub receipt_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectWriteLaneReceipt {
|
||||
pub schema: &'static str,
|
||||
pub state: &'static str,
|
||||
pub account_key: String,
|
||||
pub lane_id: Option<String>,
|
||||
pub owner_instance_id: Option<String>,
|
||||
pub acquired_at_unix_ms: Option<u128>,
|
||||
pub observed_at_unix_ms: u128,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ActiveWriterRecord {
|
||||
schema: String,
|
||||
account_key: String,
|
||||
lane_id: String,
|
||||
owner_instance_id: String,
|
||||
resume_token_sha256: String,
|
||||
acquired_at_unix_ms: u128,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn acquire_development_write_lane(
|
||||
app: AppHandle,
|
||||
input: AcquireWriteLaneInput,
|
||||
) -> Result<WriteLaneReceipt, String> {
|
||||
let root = bridge_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || acquire_at(&root, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn inspect_development_write_lane(
|
||||
app: AppHandle,
|
||||
input: InspectWriteLaneInput,
|
||||
) -> Result<InspectWriteLaneReceipt, String> {
|
||||
let root = bridge_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || inspect_at(&root, &input.account_id))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn release_development_write_lane(
|
||||
app: AppHandle,
|
||||
input: ReleaseWriteLaneInput,
|
||||
) -> Result<WriteLaneReceipt, String> {
|
||||
let root = bridge_root(&app)?;
|
||||
tauri::async_runtime::spawn_blocking(move || release_at(&root, input))
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
fn bridge_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let app_data = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
|
||||
let root = app_data.join("local-development-bridge-v1");
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
fn acquire_at(root: &Path, input: AcquireWriteLaneInput) -> Result<WriteLaneReceipt, String> {
|
||||
validate_identifier(&input.account_id, "ACCOUNT")?;
|
||||
validate_identifier(&input.lane_id, "LANE")?;
|
||||
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
||||
|
||||
let account_key = sha256_hex(input.account_id.as_bytes());
|
||||
let account_root = prepare_account_root(root, &account_key)?;
|
||||
let active_path = account_root.join("active-writer.json");
|
||||
|
||||
if active_path.exists() {
|
||||
let active = read_active_record(&active_path)?;
|
||||
return resume_existing(active, &input.lane_id, input.resume_token.as_deref());
|
||||
}
|
||||
|
||||
if input.resume_token.is_some() {
|
||||
return Err("HOLOLAKE_WRITER_RESUME_TOKEN_STALE".into());
|
||||
}
|
||||
|
||||
let resume_token = Uuid::new_v4().to_string();
|
||||
let acquired_at_unix_ms = now_unix_ms()?;
|
||||
let record = ActiveWriterRecord {
|
||||
schema: BRIDGE_SCHEMA.to_string(),
|
||||
account_key: account_key.clone(),
|
||||
lane_id: input.lane_id.clone(),
|
||||
owner_instance_id: input.owner_instance_id.clone(),
|
||||
resume_token_sha256: sha256_hex(resume_token.as_bytes()),
|
||||
acquired_at_unix_ms,
|
||||
};
|
||||
let bytes = serde_json::to_vec_pretty(&record)
|
||||
.map_err(|error| format!("HOLOLAKE_WRITER_RECORD_INVALID: {error}"))?;
|
||||
let mut file = match OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&active_path)
|
||||
{
|
||||
Ok(file) => file,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
let active = read_active_record(&active_path)?;
|
||||
return resume_existing(active, &input.lane_id, input.resume_token.as_deref());
|
||||
}
|
||||
Err(error) => return Err(format!("HOLOLAKE_WRITER_ACQUIRE_FAILED: {error}")),
|
||||
};
|
||||
file.write_all(&bytes)
|
||||
.and_then(|_| file.sync_all())
|
||||
.map_err(|error| format!("HOLOLAKE_WRITER_ACQUIRE_FAILED: {error}"))?;
|
||||
|
||||
Ok(receipt_for(
|
||||
"ACQUIRED",
|
||||
&record,
|
||||
Some(resume_token),
|
||||
acquired_at_unix_ms,
|
||||
))
|
||||
}
|
||||
|
||||
fn resume_existing(
|
||||
record: ActiveWriterRecord,
|
||||
requested_lane_id: &str,
|
||||
resume_token: Option<&str>,
|
||||
) -> Result<WriteLaneReceipt, String> {
|
||||
if record.lane_id != requested_lane_id {
|
||||
return Err("HOLOLAKE_ACCOUNT_WRITER_ALREADY_ACTIVE".into());
|
||||
}
|
||||
let supplied = resume_token.ok_or("HOLOLAKE_ACCOUNT_WRITER_ALREADY_ACTIVE")?;
|
||||
if sha256_hex(supplied.as_bytes()) != record.resume_token_sha256 {
|
||||
return Err("HOLOLAKE_ACCOUNT_WRITER_ALREADY_ACTIVE".into());
|
||||
}
|
||||
let observed_at = now_unix_ms()?;
|
||||
Ok(receipt_for("RESUMED", &record, None, observed_at))
|
||||
}
|
||||
|
||||
fn inspect_at(root: &Path, account_id: &str) -> Result<InspectWriteLaneReceipt, String> {
|
||||
validate_identifier(account_id, "ACCOUNT")?;
|
||||
let account_key = sha256_hex(account_id.as_bytes());
|
||||
let active_path = root
|
||||
.join("accounts")
|
||||
.join(&account_key)
|
||||
.join("active-writer.json");
|
||||
let observed_at_unix_ms = now_unix_ms()?;
|
||||
if !active_path.exists() {
|
||||
return Ok(InspectWriteLaneReceipt {
|
||||
schema: BRIDGE_SCHEMA,
|
||||
state: "AVAILABLE",
|
||||
account_key,
|
||||
lane_id: None,
|
||||
owner_instance_id: None,
|
||||
acquired_at_unix_ms: None,
|
||||
observed_at_unix_ms,
|
||||
});
|
||||
}
|
||||
let record = read_active_record(&active_path)?;
|
||||
Ok(InspectWriteLaneReceipt {
|
||||
schema: BRIDGE_SCHEMA,
|
||||
state: "ACTIVE",
|
||||
account_key,
|
||||
lane_id: Some(record.lane_id),
|
||||
owner_instance_id: Some(record.owner_instance_id),
|
||||
acquired_at_unix_ms: Some(record.acquired_at_unix_ms),
|
||||
observed_at_unix_ms,
|
||||
})
|
||||
}
|
||||
|
||||
fn release_at(root: &Path, input: ReleaseWriteLaneInput) -> Result<WriteLaneReceipt, String> {
|
||||
validate_identifier(&input.account_id, "ACCOUNT")?;
|
||||
validate_identifier(&input.lane_id, "LANE")?;
|
||||
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
||||
let account_key = sha256_hex(input.account_id.as_bytes());
|
||||
let account_root = root.join("accounts").join(&account_key);
|
||||
let active_path = account_root.join("active-writer.json");
|
||||
let record = read_active_record(&active_path)?;
|
||||
if record.lane_id != input.lane_id
|
||||
|| record.owner_instance_id != input.owner_instance_id
|
||||
|| record.resume_token_sha256 != sha256_hex(input.resume_token.as_bytes())
|
||||
{
|
||||
return Err("HOLOLAKE_WRITER_RELEASE_NOT_AUTHORIZED".into());
|
||||
}
|
||||
|
||||
let released_at = now_unix_ms()?;
|
||||
let receipt_id = receipt_id("RELEASED", &record, released_at);
|
||||
let receipts_root = account_root.join("receipts");
|
||||
fs::create_dir_all(&receipts_root)
|
||||
.map_err(|error| format!("HOLOLAKE_WRITER_RELEASE_FAILED: {error}"))?;
|
||||
let released_path = receipts_root.join(format!("{receipt_id}.json"));
|
||||
fs::rename(&active_path, &released_path)
|
||||
.map_err(|error| format!("HOLOLAKE_WRITER_RELEASE_FAILED: {error}"))?;
|
||||
|
||||
Ok(WriteLaneReceipt {
|
||||
schema: BRIDGE_SCHEMA,
|
||||
state: "RELEASED",
|
||||
account_key: record.account_key,
|
||||
lane_id: record.lane_id,
|
||||
owner_instance_id: record.owner_instance_id,
|
||||
acquired_at_unix_ms: record.acquired_at_unix_ms,
|
||||
observed_at_unix_ms: released_at,
|
||||
resume_token: None,
|
||||
receipt_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_account_root(root: &Path, account_key: &str) -> Result<PathBuf, String> {
|
||||
let accounts_root = root.join("accounts");
|
||||
fs::create_dir_all(&accounts_root)
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let account_root = accounts_root.join(account_key);
|
||||
fs::create_dir_all(&account_root)
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
Ok(account_root)
|
||||
}
|
||||
|
||||
fn read_active_record(path: &Path) -> Result<ActiveWriterRecord, String> {
|
||||
let bytes = fs::read(path).map_err(|error| {
|
||||
if error.kind() == std::io::ErrorKind::NotFound {
|
||||
"HOLOLAKE_WRITER_NOT_ACTIVE".to_string()
|
||||
} else {
|
||||
format!("HOLOLAKE_WRITER_RECORD_UNREADABLE: {error}")
|
||||
}
|
||||
})?;
|
||||
let record: ActiveWriterRecord = serde_json::from_slice(&bytes)
|
||||
.map_err(|error| format!("HOLOLAKE_WRITER_RECORD_INVALID: {error}"))?;
|
||||
if record.schema != BRIDGE_SCHEMA {
|
||||
return Err("HOLOLAKE_WRITER_RECORD_SCHEMA_UNSUPPORTED".into());
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn receipt_for(
|
||||
state: &'static str,
|
||||
record: &ActiveWriterRecord,
|
||||
resume_token: Option<String>,
|
||||
observed_at_unix_ms: u128,
|
||||
) -> WriteLaneReceipt {
|
||||
WriteLaneReceipt {
|
||||
schema: BRIDGE_SCHEMA,
|
||||
state,
|
||||
account_key: record.account_key.clone(),
|
||||
lane_id: record.lane_id.clone(),
|
||||
owner_instance_id: record.owner_instance_id.clone(),
|
||||
acquired_at_unix_ms: record.acquired_at_unix_ms,
|
||||
observed_at_unix_ms,
|
||||
resume_token,
|
||||
receipt_id: receipt_id(state, record, observed_at_unix_ms),
|
||||
}
|
||||
}
|
||||
|
||||
fn receipt_id(state: &str, record: &ActiveWriterRecord, observed_at_unix_ms: u128) -> String {
|
||||
sha256_hex(
|
||||
format!(
|
||||
"{state}\n{}\n{}\n{}\n{}\n{observed_at_unix_ms}",
|
||||
record.account_key,
|
||||
record.lane_id,
|
||||
record.owner_instance_id,
|
||||
record.acquired_at_unix_ms
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_identifier(value: &str, kind: &str) -> Result<(), String> {
|
||||
if value.is_empty()
|
||||
|| value.len() > MAX_ID_BYTES
|
||||
|| !value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
|
||||
{
|
||||
return Err(format!("HOLOLAKE_{kind}_ID_INVALID"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> Result<u128, String> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis())
|
||||
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn sha256_hex(value: &[u8]) -> String {
|
||||
digest(&SHA256, value)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn acquire_input(lane_id: &str) -> AcquireWriteLaneInput {
|
||||
AcquireWriteLaneInput {
|
||||
account_id: "human-BS-0001".into(),
|
||||
lane_id: lane_id.into(),
|
||||
owner_instance_id: "codex-instance-1".into(),
|
||||
resume_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_account_cannot_open_two_write_lanes() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let first = acquire_at(temp.path(), acquire_input("DEV-001")).unwrap();
|
||||
assert_eq!(first.state, "ACQUIRED");
|
||||
assert_eq!(
|
||||
acquire_at(temp.path(), acquire_input("DEV-002")).unwrap_err(),
|
||||
"HOLOLAKE_ACCOUNT_WRITER_ALREADY_ACTIVE"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_lane_resumes_after_the_ai_reconnects() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let first = acquire_at(temp.path(), acquire_input("DEV-001")).unwrap();
|
||||
let mut reconnect = acquire_input("DEV-001");
|
||||
reconnect.resume_token = first.resume_token.clone();
|
||||
let resumed = acquire_at(temp.path(), reconnect).unwrap();
|
||||
assert_eq!(resumed.state, "RESUMED");
|
||||
assert_eq!(resumed.lane_id, "DEV-001");
|
||||
assert!(resumed.resume_token.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resume_token_cannot_be_used_to_switch_to_another_lane() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let first = acquire_at(temp.path(), acquire_input("DEV-001")).unwrap();
|
||||
let mut impostor = acquire_input("DEV-002");
|
||||
impostor.resume_token = first.resume_token;
|
||||
assert_eq!(
|
||||
acquire_at(temp.path(), impostor).unwrap_err(),
|
||||
"HOLOLAKE_ACCOUNT_WRITER_ALREADY_ACTIVE"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_release_creates_a_receipt_then_allows_the_next_lane() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let first = acquire_at(temp.path(), acquire_input("DEV-001")).unwrap();
|
||||
let released = release_at(
|
||||
temp.path(),
|
||||
ReleaseWriteLaneInput {
|
||||
account_id: "human-BS-0001".into(),
|
||||
lane_id: "DEV-001".into(),
|
||||
owner_instance_id: "codex-instance-1".into(),
|
||||
resume_token: first.resume_token.unwrap(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(released.state, "RELEASED");
|
||||
assert!(temp
|
||||
.path()
|
||||
.join("accounts")
|
||||
.join(released.account_key)
|
||||
.join("receipts")
|
||||
.join(format!("{}.json", released.receipt_id))
|
||||
.is_file());
|
||||
|
||||
let second = acquire_at(temp.path(), acquire_input("DEV-002")).unwrap();
|
||||
assert_eq!(second.state, "ACQUIRED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_never_discloses_the_resume_token() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
acquire_at(temp.path(), acquire_input("DEV-001")).unwrap();
|
||||
let observed = inspect_at(temp.path(), "human-BS-0001").unwrap();
|
||||
assert_eq!(observed.state, "ACTIVE");
|
||||
assert_eq!(observed.lane_id.as_deref(), Some("DEV-001"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue