feat(hololake): implement GHS-014 stage-one home
This commit is contained in:
parent
f4c896d15c
commit
4d2415a35b
15 changed files with 891 additions and 49 deletions
|
|
@ -27,7 +27,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
mpsc, Arc,
|
||||
};
|
||||
use std::thread;
|
||||
|
|
@ -40,10 +40,33 @@ const MAX_REQUEST_BYTES: u64 = 1024 * 1024;
|
|||
|
||||
pub struct DirectLocalBrokerHandle {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
authenticated_connections: Arc<AtomicUsize>,
|
||||
socket_path: PathBuf,
|
||||
worker: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl DirectLocalBrokerHandle {
|
||||
pub fn active_connection_count(&self) -> usize {
|
||||
self.authenticated_connections.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticatedConnectionGuard(Arc<AtomicUsize>);
|
||||
|
||||
impl Drop for AuthenticatedConnectionGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
struct BrokerStorageRoots {
|
||||
session: PathBuf,
|
||||
routing: PathBuf,
|
||||
pncc_mount: PathBuf,
|
||||
pncc_remote: PathBuf,
|
||||
pncc_projection: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for DirectLocalBrokerHandle {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown.store(true, Ordering::Release);
|
||||
|
|
@ -296,7 +319,9 @@ fn start_at(
|
|||
)?;
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let authenticated_connections = Arc::new(AtomicUsize::new(0));
|
||||
let worker_shutdown = Arc::clone(&shutdown);
|
||||
let worker_authenticated_connections = Arc::clone(&authenticated_connections);
|
||||
let worker_socket = socket_path.clone();
|
||||
let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
|
||||
let worker = thread::Builder::new()
|
||||
|
|
@ -305,12 +330,15 @@ fn start_at(
|
|||
let _ = ready_sender.send(());
|
||||
serve(
|
||||
listener,
|
||||
session_root,
|
||||
routing_root,
|
||||
pncc_mount_root,
|
||||
pncc_remote_root,
|
||||
pncc_projection_root,
|
||||
BrokerStorageRoots {
|
||||
session: session_root,
|
||||
routing: routing_root,
|
||||
pncc_mount: pncc_mount_root,
|
||||
pncc_remote: pncc_remote_root,
|
||||
pncc_projection: pncc_projection_root,
|
||||
},
|
||||
&worker_shutdown,
|
||||
&worker_authenticated_connections,
|
||||
);
|
||||
let _ = fs::remove_file(worker_socket);
|
||||
})
|
||||
|
|
@ -321,6 +349,7 @@ fn start_at(
|
|||
|
||||
Ok(DirectLocalBrokerHandle {
|
||||
shutdown,
|
||||
authenticated_connections,
|
||||
socket_path,
|
||||
worker: Some(worker),
|
||||
})
|
||||
|
|
@ -328,12 +357,9 @@ fn start_at(
|
|||
|
||||
fn serve(
|
||||
listener: UnixListener,
|
||||
session_root: PathBuf,
|
||||
routing_root: PathBuf,
|
||||
pncc_mount_root: PathBuf,
|
||||
pncc_remote_root: PathBuf,
|
||||
pncc_projection_root: PathBuf,
|
||||
roots: BrokerStorageRoots,
|
||||
shutdown: &AtomicBool,
|
||||
authenticated_connections: &Arc<AtomicUsize>,
|
||||
) {
|
||||
while !shutdown.load(Ordering::Acquire) {
|
||||
match listener.accept() {
|
||||
|
|
@ -341,11 +367,15 @@ fn serve(
|
|||
if shutdown.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let root = session_root.clone();
|
||||
let routes = routing_root.clone();
|
||||
let pncc_mounts = pncc_mount_root.clone();
|
||||
let pncc_remote = pncc_remote_root.clone();
|
||||
let pncc_projection = pncc_projection_root.clone();
|
||||
if stream.set_nonblocking(false).is_err() {
|
||||
continue;
|
||||
}
|
||||
let root = roots.session.clone();
|
||||
let routes = roots.routing.clone();
|
||||
let pncc_mounts = roots.pncc_mount.clone();
|
||||
let pncc_remote = roots.pncc_remote.clone();
|
||||
let pncc_projection = roots.pncc_projection.clone();
|
||||
let client_authenticated_connections = Arc::clone(authenticated_connections);
|
||||
let _ = thread::Builder::new()
|
||||
.name("hololake-direct-local-client".into())
|
||||
.spawn(move || {
|
||||
|
|
@ -356,6 +386,7 @@ fn serve(
|
|||
&pncc_mounts,
|
||||
&pncc_remote,
|
||||
&pncc_projection,
|
||||
client_authenticated_connections,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
@ -374,12 +405,14 @@ fn serve_connection(
|
|||
pncc_mount_root: &Path,
|
||||
pncc_remote_root: &Path,
|
||||
pncc_projection_root: &Path,
|
||||
authenticated_connections: Arc<AtomicUsize>,
|
||||
) {
|
||||
let read_stream = match stream.try_clone() {
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut reader = BufReader::new(read_stream);
|
||||
let mut authenticated_connection = None;
|
||||
loop {
|
||||
let mut bytes = Vec::new();
|
||||
let read = match reader
|
||||
|
|
@ -393,6 +426,7 @@ fn serve_connection(
|
|||
if read == 0 {
|
||||
return;
|
||||
}
|
||||
let request_authenticates = request_establishes_authenticated_connection(&bytes);
|
||||
let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") {
|
||||
BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE")
|
||||
} else {
|
||||
|
|
@ -405,6 +439,12 @@ fn serve_connection(
|
|||
&bytes[..bytes.len() - 1],
|
||||
)
|
||||
};
|
||||
if response.ok && request_authenticates && authenticated_connection.is_none() {
|
||||
authenticated_connections.fetch_add(1, Ordering::AcqRel);
|
||||
authenticated_connection = Some(AuthenticatedConnectionGuard(Arc::clone(
|
||||
&authenticated_connections,
|
||||
)));
|
||||
}
|
||||
if serde_json::to_writer(&mut stream, &response).is_err()
|
||||
|| stream.write_all(b"\n").is_err()
|
||||
|| stream.flush().is_err()
|
||||
|
|
@ -417,6 +457,18 @@ fn serve_connection(
|
|||
}
|
||||
}
|
||||
|
||||
fn request_establishes_authenticated_connection(bytes: &[u8]) -> bool {
|
||||
serde_json::from_slice::<serde_json::Value>(bytes)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("operation")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.is_some_and(|operation| matches!(operation.as_str(), "OPEN_SESSION" | "RESUME_SESSION"))
|
||||
}
|
||||
|
||||
fn dispatch(
|
||||
session_root: &Path,
|
||||
routing_root: &Path,
|
||||
|
|
@ -840,4 +892,68 @@ mod tests {
|
|||
assert_eq!(verified["result"]["emptyMeansOffline"], false);
|
||||
assert_eq!(verified["result"]["returnedEventCount"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_authenticated_persistent_connector_counts_as_online() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let socket = temp.path().join("runtime/broker.sock");
|
||||
let descriptor = temp.path().join("broker.json");
|
||||
let sessions = temp.path().join("sessions");
|
||||
let routes = temp.path().join("routes");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
fs::create_dir_all(&routes).unwrap();
|
||||
let broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
|
||||
|
||||
let probe = UnixStream::connect(&socket).unwrap();
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
assert_eq!(broker.active_connection_count(), 0);
|
||||
drop(probe);
|
||||
|
||||
let ticket = issue_ticket_at(
|
||||
&sessions,
|
||||
IssueDiscoveryTicketInput {
|
||||
account_id: "human-1".into(),
|
||||
lane_id: "DEV-1".into(),
|
||||
client_instance_id: "codex-1".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut connector = UnixStream::connect(&socket).unwrap();
|
||||
serde_json::to_writer(
|
||||
&mut connector,
|
||||
&serde_json::json!({
|
||||
"operation": "OPEN_SESSION",
|
||||
"input": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "DEV-1",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
connector.write_all(b"\n").unwrap();
|
||||
assert!(request_establishes_authenticated_connection(
|
||||
br#"{"operation":"OPEN_SESSION"}"#
|
||||
));
|
||||
let mut response = String::new();
|
||||
let mut response_reader = BufReader::new(connector.try_clone().unwrap());
|
||||
response_reader.read_line(&mut response).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&response).unwrap()["ok"],
|
||||
true
|
||||
);
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
assert_eq!(broker.active_connection_count(), 1);
|
||||
|
||||
drop(connector);
|
||||
drop(response_reader);
|
||||
for _ in 0..20 {
|
||||
if broker.active_connection_count() == 0 {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(broker.active_connection_count(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,6 +203,43 @@ pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn active_session_count_at(root: &Path) -> Result<usize, String> {
|
||||
let accounts = root.join("accounts");
|
||||
if !accounts.exists() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(&accounts)
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?
|
||||
{
|
||||
let entry = entry
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let kind = entry
|
||||
.file_type()
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
if !kind.is_dir() || kind.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
let active_path = entry.path().join("active-session.json");
|
||||
if !active_path.exists() {
|
||||
continue;
|
||||
}
|
||||
let metadata = fs::symlink_metadata(&active_path)
|
||||
.map_err(|error| format!("HOLOLAKE_ACTIVE_SESSION_INVALID: {error}"))?;
|
||||
if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
|
||||
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
|
||||
}
|
||||
let active: ActiveSessionRecord = read_json(&active_path, "ACTIVE_SESSION")?;
|
||||
if active.schema != SESSION_SCHEMA
|
||||
|| active.account_key != entry.file_name().to_string_lossy()
|
||||
{
|
||||
return Err("HOLOLAKE_ACTIVE_SESSION_INVALID".into());
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn open_at(
|
||||
root: &Path,
|
||||
input: OpenSessionInput,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::direct_local_broker::DirectLocalBrokerHandle;
|
||||
use crate::direct_local_session::{active_session_count_at, direct_session_root};
|
||||
use crate::pncc_receipt_projection::{pncc_projection_root, projection_event_count_at};
|
||||
use crate::pncc_repository_binding::{mounted_repository_count_at, pncc_repository_mount_root};
|
||||
use crate::release_trust::release_trust_state;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HoloLakeHomeStatus {
|
||||
pub schema: &'static str,
|
||||
pub direct_local_broker_state: &'static str,
|
||||
pub direct_connection_count: usize,
|
||||
pub resumable_session_count: usize,
|
||||
pub code_repository_mount_count: usize,
|
||||
pub pncc_receipt_count: usize,
|
||||
pub update_state: &'static str,
|
||||
pub automatic_upstream_updates: bool,
|
||||
pub mcp_role: &'static str,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_hololake_home_status(
|
||||
app: AppHandle,
|
||||
broker: State<'_, DirectLocalBrokerHandle>,
|
||||
) -> Result<HoloLakeHomeStatus, String> {
|
||||
let session_root = direct_session_root(&app)?;
|
||||
let mount_root = pncc_repository_mount_root(&app)?;
|
||||
let projection_root = pncc_projection_root(&app)?;
|
||||
Ok(HoloLakeHomeStatus {
|
||||
schema: "hololake.home-status/v1",
|
||||
direct_local_broker_state: "READY",
|
||||
direct_connection_count: broker.active_connection_count(),
|
||||
resumable_session_count: active_session_count_at(&session_root)?,
|
||||
code_repository_mount_count: mounted_repository_count_at(&mount_root)?,
|
||||
pncc_receipt_count: projection_event_count_at(&projection_root)?,
|
||||
update_state: release_trust_state()?,
|
||||
automatic_upstream_updates: false,
|
||||
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
mod direct_local_broker;
|
||||
mod direct_local_session;
|
||||
mod dynamic_capability_routing;
|
||||
mod home_status;
|
||||
mod local_development_bridge;
|
||||
mod pncc_receipt_projection;
|
||||
mod pncc_remote_git;
|
||||
|
|
@ -17,6 +18,7 @@ pub fn run_connector() -> Result<(), String> {
|
|||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
home_status::get_hololake_home_status,
|
||||
direct_local_session::issue_direct_local_discovery_ticket,
|
||||
direct_local_session::open_direct_local_session,
|
||||
direct_local_session::resume_direct_local_session,
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn projection_event_count_at(root: &Path) -> Result<usize, String> {
|
||||
let _lock = lock_projection(root)?;
|
||||
Ok(load_and_verify(root)?.events.len())
|
||||
}
|
||||
|
||||
pub(crate) fn append_repository_binding_at(
|
||||
root: &Path,
|
||||
receipt: &PnccRepositoryMountReceipt,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,32 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
|
|||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn mounted_repository_count_at(root: &Path) -> Result<usize, String> {
|
||||
let mut count = 0;
|
||||
for entry in fs::read_dir(root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?
|
||||
{
|
||||
let entry =
|
||||
entry.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let kind = entry
|
||||
.file_type()
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
let path = entry.path();
|
||||
if kind.is_symlink()
|
||||
|| !kind.is_file()
|
||||
|| path.extension().and_then(|value| value.to_str()) != Some("json")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let record: PnccRepositoryMountRecord = read_json(&path)?;
|
||||
if record.schema != MOUNT_SCHEMA || mount_path(root, &record.mount_id) != path {
|
||||
return Err("PNCC_REPOSITORY_MOUNT_RECORD_INVALID".into());
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) fn inspect_mounted_at(
|
||||
root: &Path,
|
||||
input: InspectMountedPnccRepositoryInput,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ enum ValidatedReleaseTrust {
|
|||
},
|
||||
}
|
||||
|
||||
pub(crate) fn release_trust_state() -> Result<&'static str, String> {
|
||||
match validate_release_trust(EMBEDDED_RELEASE_TRUST)? {
|
||||
ValidatedReleaseTrust::Disabled => Ok("UNPROVISIONED_FAIL_CLOSED"),
|
||||
ValidatedReleaseTrust::Enabled { .. } => Ok("READY_HUMAN_CONFIRMATION_REQUIRED"),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_release_trust(raw: &str) -> Result<ValidatedReleaseTrust, String> {
|
||||
let trust: ReleaseTrust = serde_json::from_str(raw)
|
||||
.map_err(|error| format!("invalid HoloLake release trust document: {error}"))?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue