feat(hololake): add signed dynamic capability routing

This commit is contained in:
冰朔 2026-08-13 12:45:35 +08:00
commit ec8bbfd604
12 changed files with 1085 additions and 17 deletions

View file

@ -0,0 +1,50 @@
{
"schema": "hololake.dynamic-capability-routing-contract/v1",
"record_id": "HLP-NATIVE-DYNAMIC-CAPABILITY-ROUTING-001",
"state": "LOCAL_SIGNED_REGISTRY_AND_HEALTH_ROUTING_IMPLEMENTED_PRODUCTION_WRITE_ADMISSION_DISABLED",
"purpose": "Dynamically select a registered team or user-owned node for an exact capability without hosting the user's private work lake or carrying workload concurrency in HoloLake.",
"registry": {
"owner": "HOLOLAKE_NATIVE_LOCAL_CORE",
"dynamic_node_count": true,
"fixed_node_slots": false,
"endpoint_hardcoding_allowed": false,
"platform_carries_user_workload_concurrency": false,
"node_private_keys_leave_node": false,
"registry_mutation_requires_trusted_signature": true,
"signer_trust_is_external_to_registry_payload": true,
"unprovisioned_trust_fails_closed": true,
"webview_registry_or_health_mutation_allowed": false,
"production_registry_source": "REPO_012_OR_SIGNED_HOLOLAKE_BROADCAST"
},
"route_resolution": {
"required_bindings": [
"HUMAN_ID",
"PERSONA_ID",
"DOMAIN_ID",
"CAPABILITY_ID",
"NODE_PROTOCOL_PUBLIC_KEY",
"FRESH_SIGNED_HEALTH_RECEIPT"
],
"ssh_or_root_is_identity": false,
"stale_or_unsigned_health_is_eligible": false,
"write_authority_from_health_receipt": false,
"deterministic_selection": "PRIORITY_THEN_NODE_ID",
"empty_route_fails_closed": true
,"external_ai_route_requires_active_local_session": true
},
"health_receipt": {
"signature": "ED25519",
"canonical_bytes": "SCHEMA_NODE_REGISTRY_VERSION_NODE_ID_OBSERVED_AT_EXPIRES_AT_SEQUENCE_STATUS_CAPABILITIES",
"replay_sequence_strictly_increases": true,
"future_clock_skew_ms": 300000,
"status_required": "READY"
},
"five_domain_visibility": "SUBMERGED_SYSTEM_FOUNDATION_NOT_STAGE_ONE_PRIMARY_NAVIGATION",
"production_truth": {
"jd_fd_primary_system_claimed": true,
"jd_fd_primary_transport_key_bound": false,
"jd_fd_primary_write_admission_runtime": false,
"enterprise_nodes_claimed": 0,
"production_health_receipts_observed": 0
}
}

View file

@ -0,0 +1,15 @@
# HoloLake Native Desktop Architecture
The stage-one shell is a Tauri v2 application with a Rust-owned local core and a React human projection. The five domains and their server fleet remain submerged system infrastructure rather than primary stage-one navigation.
## External programming AI entry
MCP may discover HoloLake, but it does not own continuity. The installed application starts a user-only Unix socket broker. A programming AI opens or resumes a HoloLake-issued local session, then uses the installed executable's `--connector` mode for newline-delimited protocol traffic. Session secrets are stored only as hashes. Events use exact cursors and idempotency keys.
## Dynamic capability routing
HoloLake stores a signed node capability registry rather than fixed server slots. A candidate route must bind the exact human, persona, domain and capability. It is eligible only while a fresh node-signed health receipt reports the capability as ready. Selection is deterministic by priority and node id.
The registry signer trust root is embedded independently from registry payloads and is currently `UNPROVISIONED_FAIL_CLOSED`. Registry and health mutation are not exposed to the webview. External AI route queries require possession of the active HoloLake local session secret. A route receipt never grants write authority; production node write admission remains a separate unimplemented gate.
HoloLake routes work but does not host the user's private work lake, pool users into a multi-tenant runtime, or carry server workload concurrency.

View file

@ -0,0 +1,22 @@
# ADR 0001: Signed dynamic capability routing
Status: Accepted locally; production trust provisioning pending.
## Context
Stage one must connect a changing fleet of team and user-owned nodes without hardcoded endpoints. SSH access, root access, a URL, or a health probe cannot establish Guanghu node identity or write authority. The client must also preserve the one-human-one-owned-root-node boundary and avoid becoming a hosted concurrency layer.
## Decision
Use a Rust-owned, file-backed dynamic registry whose complete canonical payload is verified with an independently embedded Ed25519 trust root. Each node carries a protocol public key and explicit human, persona, domain and capability bindings. Runtime eligibility additionally requires a fresh Ed25519 health receipt from that node with a strictly increasing sequence.
Route resolution is available to external programming AI only through an authenticated active local HoloLake session. It returns a non-authorizing route receipt. Webview code cannot install registries or submit health receipts.
The embedded trust file initially contains no signer. This makes production registry installation fail closed until the registered HoloLake/JD-controller signing identity is provisioned.
## Consequences
- The fleet can grow without client code changes or fixed server slots.
- Stale, replayed, unsigned or incorrectly bound nodes are not routable.
- Registry publication, health, route selection and write admission remain separate facts.
- Production node enrollment and write execution still require their own source, signing, deployment and readback receipts.

View file

@ -1,7 +1,7 @@
{ {
"schema": "hololake.native-desktop-foundation/v1", "schema": "hololake.native-desktop-foundation/v1",
"record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001", "record_id": "HLP-NATIVE-DESKTOP-FOUNDATION-001",
"state": "TAURI_FOUNDATION_WITH_ACCOUNT_SINGLE_WRITER_AND_DIRECT_LOCAL_BROKER", "state": "TAURI_FOUNDATION_WITH_DIRECT_LOCAL_BROKER_AND_DYNAMIC_CAPABILITY_ROUTING",
"canonical_shell": "TAURI_V2_RUST_REACT", "canonical_shell": "TAURI_V2_RUST_REACT",
"product_ui_implementation_started": false, "product_ui_implementation_started": false,
"selected_visual_direction_present": false, "selected_visual_direction_present": false,
@ -24,6 +24,12 @@
"account_single_writer_kernel_implemented": true, "account_single_writer_kernel_implemented": true,
"external_local_broker_implemented": true, "external_local_broker_implemented": true,
"resumable_direct_local_session_kernel_implemented": true, "resumable_direct_local_session_kernel_implemented": true,
"dynamic_capability_routing_contract": "contracts/dynamic-capability-routing.json",
"dynamic_capability_registry_implemented": true,
"signed_node_health_routing_implemented": true,
"dynamic_routing_trust_state": "UNPROVISIONED_FAIL_CLOSED",
"dynamic_routing_webview_mutation_exposed": false,
"production_node_write_admission_implemented": false,
"stage_one_product_contract_state": "TCS_UNIVERSAL_LANGUAGE_PERSONAL_CHANNEL_CONTRACT_ONLY_RUNTIME_NOT_IMPLEMENTED", "stage_one_product_contract_state": "TCS_UNIVERSAL_LANGUAGE_PERSONAL_CHANNEL_CONTRACT_ONLY_RUNTIME_NOT_IMPLEMENTED",
"stage_one_internal_ai_interaction": false, "stage_one_internal_ai_interaction": false,
"stage_one_model_api_configuration": false, "stage_one_model_api_configuration": false,

View file

@ -0,0 +1,40 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
const contract = JSON.parse(
fs.readFileSync(new URL('../contracts/dynamic-capability-routing.json', import.meta.url), 'utf8'),
)
const foundation = JSON.parse(
fs.readFileSync(new URL('../foundation.json', import.meta.url), 'utf8'),
)
test('server capability routing is dynamic and never hardcodes a finite fleet', () => {
assert.equal(contract.registry.dynamic_node_count, true)
assert.equal(contract.registry.fixed_node_slots, false)
assert.equal(contract.registry.endpoint_hardcoding_allowed, false)
assert.equal(contract.registry.signer_trust_is_external_to_registry_payload, true)
assert.equal(contract.registry.unprovisioned_trust_fails_closed, true)
assert.equal(contract.registry.webview_registry_or_health_mutation_allowed, false)
assert.equal(contract.registry.platform_carries_user_workload_concurrency, false)
})
test('routing requires exact human persona domain capability and signed health bindings', () => {
assert.deepEqual(contract.route_resolution.required_bindings, [
'HUMAN_ID',
'PERSONA_ID',
'DOMAIN_ID',
'CAPABILITY_ID',
'NODE_PROTOCOL_PUBLIC_KEY',
'FRESH_SIGNED_HEALTH_RECEIPT',
])
assert.equal(contract.route_resolution.ssh_or_root_is_identity, false)
assert.equal(contract.route_resolution.stale_or_unsigned_health_is_eligible, false)
assert.equal(contract.route_resolution.external_ai_route_requires_active_local_session, true)
})
test('the native mainline reports only implemented dynamic routing capabilities', () => {
assert.equal(foundation.dynamic_capability_registry_implemented, true)
assert.equal(foundation.signed_node_health_routing_implemented, true)
assert.equal(foundation.production_node_write_admission_implemented, false)
})

View file

@ -1345,6 +1345,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
name = "hololake-native-desktop" name = "hololake-native-desktop"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"dirs", "dirs",
"fs2", "fs2",
"ring", "ring",

View file

@ -18,6 +18,7 @@ tauri-build = { version = "2.5.4", features = [] }
dirs = "6" dirs = "6"
fs2 = "0.4" fs2 = "0.4"
ring = "0.17" ring = "0.17"
base64 = "0.22"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tauri = { version = "=2.10.2", features = [] } tauri = { version = "=2.10.2", features = [] }

View file

@ -0,0 +1,5 @@
{
"schema": "hololake.dynamic-routing-trust/v1",
"state": "UNPROVISIONED_FAIL_CLOSED",
"signers": []
}

View file

@ -1,6 +1,11 @@
use crate::direct_local_session::{ use crate::direct_local_session::{
append_event_at, direct_session_root, open_at, resume_at, AppendSessionEventInput, append_event_at, authenticate_at, direct_session_root, open_at, resume_at,
OpenSessionInput, ResumeSessionInput, AppendSessionEventInput, AuthenticateSessionInput, OpenSessionInput, ResumeSessionInput,
};
use crate::dynamic_capability_routing::{
install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at,
routing_root as dynamic_routing_root, DynamicNodeRegistry, ResolveCapabilityRouteInput,
SignedNodeHealth,
}; };
use ring::digest::{digest, SHA256}; use ring::digest::{digest, SHA256};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,7 +17,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, mpsc, Arc,
}; };
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@ -25,12 +30,16 @@ const MAX_REQUEST_BYTES: u64 = 1024 * 1024;
pub struct DirectLocalBrokerHandle { pub struct DirectLocalBrokerHandle {
shutdown: Arc<AtomicBool>, shutdown: Arc<AtomicBool>,
socket_path: PathBuf, socket_path: PathBuf,
worker: Option<thread::JoinHandle<()>>,
} }
impl Drop for DirectLocalBrokerHandle { impl Drop for DirectLocalBrokerHandle {
fn drop(&mut self) { fn drop(&mut self) {
self.shutdown.store(true, Ordering::Release); self.shutdown.store(true, Ordering::Release);
let _ = UnixStream::connect(&self.socket_path); let _ = UnixStream::connect(&self.socket_path);
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
} }
} }
@ -45,6 +54,30 @@ enum BrokerRequest {
OpenSession(OpenSessionInput), OpenSession(OpenSessionInput),
ResumeSession(ResumeSessionInput), ResumeSession(ResumeSessionInput),
AppendEvent(AppendSessionEventInput), AppendEvent(AppendSessionEventInput),
ResolveCapabilityRoute(AuthenticatedRouteInput),
InstallDynamicNodeRegistry(AuthenticatedRegistryInput),
RecordSignedNodeHealth(AuthenticatedHealthInput),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedRouteInput {
session: AuthenticateSessionInput,
route: ResolveCapabilityRouteInput,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedRegistryInput {
session: AuthenticateSessionInput,
registry: DynamicNodeRegistry,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedHealthInput {
session: AuthenticateSessionInput,
health: SignedNodeHealth,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -75,14 +108,16 @@ struct ConnectorDescriptor {
state: String, state: String,
transport: String, transport: String,
socket_path: String, socket_path: String,
process_id: u32,
} }
pub fn start(app: &AppHandle) -> Result<DirectLocalBrokerHandle, Box<dyn std::error::Error>> { pub fn start(app: &AppHandle) -> Result<DirectLocalBrokerHandle, Box<dyn std::error::Error>> {
let session_root = direct_session_root(app).map_err(std::io::Error::other)?; let session_root = direct_session_root(app).map_err(std::io::Error::other)?;
let routing_root = dynamic_routing_root(app).map_err(std::io::Error::other)?;
let app_data = app.path().app_data_dir()?; let app_data = app.path().app_data_dir()?;
let descriptor_path = app_data.join("direct-local-broker-v1.json"); let descriptor_path = app_data.join("direct-local-broker-v1.json");
let socket_path = short_socket_path(&app_data); let socket_path = short_socket_path(&app_data);
start_at(session_root, descriptor_path, socket_path).map_err(|error| error.into()) start_at(session_root, routing_root, descriptor_path, socket_path).map_err(|error| error.into())
} }
pub fn run_connector() -> Result<(), String> { pub fn run_connector() -> Result<(), String> {
@ -98,8 +133,7 @@ pub fn run_connector() -> Result<(), String> {
{ {
return Err("HOLOLAKE_BROKER_DESCRIPTOR_UNSUPPORTED".into()); return Err("HOLOLAKE_BROKER_DESCRIPTOR_UNSUPPORTED".into());
} }
let mut stream = UnixStream::connect(&descriptor.socket_path) let mut stream = connect_with_startup_grace(&descriptor)?;
.map_err(|error| format!("HOLOLAKE_DIRECT_LOCAL_BROKER_NOT_RUNNING: {error}"))?;
let read_stream = stream let read_stream = stream
.try_clone() .try_clone()
.map_err(|error| format!("HOLOLAKE_CONNECTOR_CLONE_FAILED: {error}"))?; .map_err(|error| format!("HOLOLAKE_CONNECTOR_CLONE_FAILED: {error}"))?;
@ -125,6 +159,34 @@ pub fn run_connector() -> Result<(), String> {
Ok(()) Ok(())
} }
fn connect_with_startup_grace(descriptor: &ConnectorDescriptor) -> Result<UnixStream, String> {
if descriptor.process_id == 0 {
return Err("HOLOLAKE_BROKER_DESCRIPTOR_UNSUPPORTED".into());
}
let mut last_error = None;
for _ in 0..50 {
match UnixStream::connect(&descriptor.socket_path) {
Ok(stream) => return Ok(stream),
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
) =>
{
last_error = Some(error);
thread::sleep(Duration::from_millis(100));
}
Err(error) => return Err(format!("HOLOLAKE_DIRECT_LOCAL_BROKER_NOT_RUNNING: {error}")),
}
}
Err(format!(
"HOLOLAKE_DIRECT_LOCAL_BROKER_NOT_RUNNING: {}",
last_error
.map(|error| error.to_string())
.unwrap_or_else(|| "startup grace exhausted".into())
))
}
fn connector_descriptor_path() -> Result<PathBuf, String> { fn connector_descriptor_path() -> Result<PathBuf, String> {
if let Some(path) = std::env::var_os("HOLOLAKE_BROKER_DESCRIPTOR") { if let Some(path) = std::env::var_os("HOLOLAKE_BROKER_DESCRIPTOR") {
return Ok(PathBuf::from(path)); return Ok(PathBuf::from(path));
@ -139,6 +201,7 @@ fn connector_descriptor_path() -> Result<PathBuf, String> {
fn start_at( fn start_at(
session_root: PathBuf, session_root: PathBuf,
routing_root: PathBuf,
descriptor_path: PathBuf, descriptor_path: PathBuf,
socket_path: PathBuf, socket_path: PathBuf,
) -> Result<DirectLocalBrokerHandle, String> { ) -> Result<DirectLocalBrokerHandle, String> {
@ -179,28 +242,43 @@ fn start_at(
let shutdown = Arc::new(AtomicBool::new(false)); let shutdown = Arc::new(AtomicBool::new(false));
let worker_shutdown = Arc::clone(&shutdown); let worker_shutdown = Arc::clone(&shutdown);
let worker_socket = socket_path.clone(); let worker_socket = socket_path.clone();
thread::Builder::new() let (ready_sender, ready_receiver) = mpsc::sync_channel(1);
let worker = thread::Builder::new()
.name("hololake-direct-local-broker".into()) .name("hololake-direct-local-broker".into())
.spawn(move || { .spawn(move || {
serve(listener, session_root, &worker_shutdown); let _ = ready_sender.send(());
serve(listener, session_root, routing_root, &worker_shutdown);
let _ = fs::remove_file(worker_socket); let _ = fs::remove_file(worker_socket);
}) })
.map_err(|error| format!("HOLOLAKE_BROKER_THREAD_FAILED: {error}"))?; .map_err(|error| format!("HOLOLAKE_BROKER_THREAD_FAILED: {error}"))?;
ready_receiver
.recv_timeout(Duration::from_secs(2))
.map_err(|error| format!("HOLOLAKE_BROKER_STARTUP_FAILED: {error}"))?;
Ok(DirectLocalBrokerHandle { Ok(DirectLocalBrokerHandle {
shutdown, shutdown,
socket_path, socket_path,
worker: Some(worker),
}) })
} }
fn serve(listener: UnixListener, session_root: PathBuf, shutdown: &AtomicBool) { fn serve(
listener: UnixListener,
session_root: PathBuf,
routing_root: PathBuf,
shutdown: &AtomicBool,
) {
while !shutdown.load(Ordering::Acquire) { while !shutdown.load(Ordering::Acquire) {
match listener.accept() { match listener.accept() {
Ok((stream, _)) => { Ok((stream, _)) => {
if shutdown.load(Ordering::Acquire) {
break;
}
let root = session_root.clone(); let root = session_root.clone();
let routes = routing_root.clone();
let _ = thread::Builder::new() let _ = thread::Builder::new()
.name("hololake-direct-local-client".into()) .name("hololake-direct-local-client".into())
.spawn(move || serve_connection(stream, &root)); .spawn(move || serve_connection(stream, &root, &routes));
} }
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(25)); thread::sleep(Duration::from_millis(25));
@ -210,7 +288,7 @@ fn serve(listener: UnixListener, session_root: PathBuf, shutdown: &AtomicBool) {
} }
} }
fn serve_connection(mut stream: UnixStream, session_root: &Path) { fn serve_connection(mut stream: UnixStream, session_root: &Path, routing_root: &Path) {
let read_stream = match stream.try_clone() { let read_stream = match stream.try_clone() {
Ok(stream) => stream, Ok(stream) => stream,
Err(_) => return, Err(_) => return,
@ -232,7 +310,7 @@ fn serve_connection(mut stream: UnixStream, session_root: &Path) {
let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") { let response = if read as u64 > MAX_REQUEST_BYTES || !bytes.ends_with(b"\n") {
BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE") BrokerResponse::error("HOLOLAKE_BROKER_REQUEST_TOO_LARGE")
} else { } else {
dispatch(session_root, &bytes[..bytes.len() - 1]) dispatch(session_root, routing_root, &bytes[..bytes.len() - 1])
}; };
if serde_json::to_writer(&mut stream, &response).is_err() if serde_json::to_writer(&mut stream, &response).is_err()
|| stream.write_all(b"\n").is_err() || stream.write_all(b"\n").is_err()
@ -246,7 +324,7 @@ fn serve_connection(mut stream: UnixStream, session_root: &Path) {
} }
} }
fn dispatch(session_root: &Path, bytes: &[u8]) -> BrokerResponse { fn dispatch(session_root: &Path, routing_root: &Path, bytes: &[u8]) -> BrokerResponse {
let request: BrokerRequest = match serde_json::from_slice(bytes) { let request: BrokerRequest = match serde_json::from_slice(bytes) {
Ok(request) => request, Ok(request) => request,
Err(error) => { Err(error) => {
@ -266,6 +344,35 @@ fn dispatch(session_root: &Path, bytes: &[u8]) -> BrokerResponse {
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::AppendEvent(input) => append_event_at(session_root, input) BrokerRequest::AppendEvent(input) => append_event_at(session_root, input)
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())), .and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::ResolveCapabilityRoute(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"));
now.and_then(|now| resolve_capability_route_at(routing_root, input.route, now))
.and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
BrokerRequest::InstallDynamicNodeRegistry(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
install_trusted_registry_at(routing_root, input.registry).and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
BrokerRequest::RecordSignedNodeHealth(input) => {
if let Err(error) = authenticate_at(session_root, &input.session) {
return BrokerResponse::error(&error);
}
record_health_at(routing_root, input.health).and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
})
}
}; };
match result { match result {
Ok(value) => BrokerResponse::success(value), Ok(value) => BrokerResponse::success(value),
@ -351,7 +458,9 @@ mod tests {
let descriptor = temp.path().join("broker.json"); let descriptor = temp.path().join("broker.json");
let sessions = temp.path().join("sessions"); let sessions = temp.path().join("sessions");
fs::create_dir_all(&sessions).unwrap(); fs::create_dir_all(&sessions).unwrap();
let _broker = start_at(sessions.clone(), descriptor, socket.clone()).unwrap(); let routes = temp.path().join("routes");
fs::create_dir_all(&routes).unwrap();
let _broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
let ticket = issue_ticket_at( let ticket = issue_ticket_at(
&sessions, &sessions,
IssueDiscoveryTicketInput { IssueDiscoveryTicketInput {
@ -407,7 +516,15 @@ mod tests {
}, },
) )
.unwrap(); .unwrap();
let first = start_at(sessions.clone(), descriptor.clone(), socket.clone()).unwrap(); let routes = temp.path().join("routes");
fs::create_dir_all(&routes).unwrap();
let first = start_at(
sessions.clone(),
routes.clone(),
descriptor.clone(),
socket.clone(),
)
.unwrap();
let opened = request( let opened = request(
&socket, &socket,
serde_json::json!({ serde_json::json!({
@ -427,7 +544,7 @@ mod tests {
} }
thread::sleep(Duration::from_millis(10)); thread::sleep(Duration::from_millis(10));
} }
let _second = start_at(sessions, descriptor, socket.clone()).unwrap(); let _second = start_at(sessions, routes, descriptor, socket.clone()).unwrap();
let resumed = request( let resumed = request(
&socket, &socket,
serde_json::json!({ serde_json::json!({
@ -442,4 +559,99 @@ mod tests {
); );
assert_eq!(resumed["result"]["state"], "RESUMED"); assert_eq!(resumed["result"]["state"], "RESUMED");
} }
#[test]
fn broker_route_resolution_requires_the_active_session_secret() {
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 response = request(
&socket,
serde_json::json!({
"operation": "RESOLVE_CAPABILITY_ROUTE",
"input": {
"session": {
"accountId": "human-1",
"sessionId": "invented-session",
"resumeSecret": "invented-secret-long-enough"
},
"route": {
"humanId": "human-1",
"personaId": "ICE-P-ZY001",
"domainId": "DOM-FIFTH-0001",
"capabilityId": "GH-PNCC-READ"
}
}
}),
);
assert_eq!(response["ok"], false);
assert_eq!(
response["error"],
"HOLOLAKE_DIRECT_ACTIVE_SESSION_NOT_FOUND"
);
}
#[test]
fn authenticated_ai_still_cannot_install_registry_before_trust_is_provisioned() {
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 ticket = issue_ticket_at(
&sessions,
IssueDiscoveryTicketInput {
account_id: "human-1".into(),
lane_id: "DEV-1".into(),
client_instance_id: "codex-1".into(),
},
)
.unwrap();
let opened = request(
&socket,
serde_json::json!({
"operation": "OPEN_SESSION",
"input": {
"accountId": "human-1",
"laneId": "DEV-1",
"clientInstanceId": "codex-1",
"discoveryTicket": ticket.discovery_ticket
}
}),
);
let response = request(
&socket,
serde_json::json!({
"operation": "INSTALL_DYNAMIC_NODE_REGISTRY",
"input": {
"session": {
"accountId": "human-1",
"sessionId": opened["result"]["sessionId"],
"resumeSecret": opened["result"]["resumeSecret"]
},
"registry": {
"schema": "hololake.dynamic-node-registry/v1",
"registryId": "candidate-registry",
"version": 1,
"nodes": [],
"signerId": "invented-signer",
"signature": "invented-signature"
}
}
}),
);
assert_eq!(response["ok"], false);
assert_eq!(
response["error"],
"HOLOLAKE_DYNAMIC_ROUTING_TRUST_UNPROVISIONED"
);
}
} }

View file

@ -62,6 +62,14 @@ pub struct AppendSessionEventInput {
pub expected_previous_sequence: u64, pub expected_previous_sequence: u64,
} }
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateSessionInput {
pub account_id: String,
pub session_id: String,
pub resume_secret: String,
}
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DirectSessionReceipt { pub struct DirectSessionReceipt {
@ -397,6 +405,23 @@ pub(crate) fn append_event_at(
Ok(event_receipt("APPENDED", &event)) Ok(event_receipt("APPENDED", &event))
} }
pub(crate) fn authenticate_at(root: &Path, input: &AuthenticateSessionInput) -> Result<(), String> {
validate_identifier(&input.account_id, "ACCOUNT")?;
validate_identifier(&input.session_id, "SESSION")?;
validate_secret(&input.resume_secret, "RESUME_SECRET")?;
let account_key = sha256_hex(input.account_id.as_bytes());
require_active_session(root, &account_key, &input.session_id)?;
let path = session_path(root, &account_key, &input.session_id);
let _lock = lock_session(&path)?;
let record = read_session(&path)?;
authorize(
&record,
&account_key,
&input.session_id,
&input.resume_secret,
)
}
fn session_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf { fn session_path(root: &Path, account_key: &str, session_id: &str) -> PathBuf {
root.join("accounts") root.join("accounts")
.join(account_key) .join(account_key)

View file

@ -0,0 +1,690 @@
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use fs2::FileExt;
use ring::signature::{UnparsedPublicKey, ED25519};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager};
use uuid::Uuid;
const REGISTRY_SCHEMA: &str = "hololake.dynamic-node-registry/v1";
const HEALTH_SCHEMA: &str = "hololake.signed-node-health/v1";
const ROUTE_SCHEMA: &str = "hololake.capability-route-receipt/v1";
const MAX_IDENTIFIER_BYTES: usize = 128;
const MAX_CLOCK_SKEW_MS: u64 = 300_000;
const MAX_NODES: usize = 4096;
const MAX_BINDINGS_PER_NODE: usize = 256;
const EMBEDDED_ROUTING_TRUST: &str = include_str!("../dynamic-routing-trust.json");
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct DynamicNodeRegistry {
schema: String,
registry_id: String,
version: u64,
nodes: Vec<DynamicNodeRecord>,
signer_id: String,
signature: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct DynamicRoutingTrust {
schema: String,
state: RoutingTrustState,
signers: Vec<RoutingTrustSigner>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum RoutingTrustState {
UnprovisionedFailClosed,
Provisioned,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct RoutingTrustSigner {
signer_id: String,
public_key: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct DynamicNodeRecord {
node_id: String,
domain_id: String,
endpoint: String,
protocol_public_key: String,
human_ids: Vec<String>,
persona_ids: Vec<String>,
capabilities: Vec<String>,
priority: u32,
enabled: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct SignedNodeHealth {
schema: String,
registry_version: u64,
node_id: String,
observed_at_unix_ms: u64,
expires_at_unix_ms: u64,
sequence: u64,
status: HealthStatus,
capabilities: Vec<String>,
signature: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum HealthStatus {
Ready,
Degraded,
Offline,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ResolveCapabilityRouteInput {
human_id: String,
persona_id: String,
domain_id: String,
capability_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CapabilityRouteReceipt {
schema: &'static str,
state: &'static str,
registry_id: String,
registry_version: u64,
node_id: String,
domain_id: String,
endpoint: String,
capability_id: String,
human_id: String,
persona_id: String,
health_sequence: u64,
health_expires_at_unix_ms: u64,
write_authority_granted: bool,
receipt_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RegistryInstallReceipt {
schema: &'static str,
state: &'static str,
registry_id: String,
version: u64,
node_count: usize,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct HealthRecordReceipt {
schema: &'static str,
state: &'static str,
node_id: String,
sequence: u64,
expires_at_unix_ms: u64,
}
pub(crate) fn install_trusted_registry_at(
root: &Path,
registry: DynamicNodeRegistry,
) -> Result<RegistryInstallReceipt, String> {
let public_key = trusted_registry_key(EMBEDDED_ROUTING_TRUST, &registry.signer_id)?;
install_registry_at(root, registry, &public_key)
}
pub(crate) fn routing_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
.join("dynamic-capability-routing-v1");
fs::create_dir_all(&root)
.map_err(|error| format!("HOLOLAKE_DYNAMIC_ROUTING_STORAGE_UNAVAILABLE: {error}"))?;
Ok(root)
}
fn install_registry_at(
root: &Path,
registry: DynamicNodeRegistry,
trusted_public_key: &str,
) -> Result<RegistryInstallReceipt, String> {
let _lock = lock_file(&root.join("registry.lock"), "REGISTRY")?;
validate_registry(&registry)?;
verify_signature(
trusted_public_key,
&registry.signature,
canonical_registry_bytes(&registry).as_bytes(),
"REGISTRY",
)?;
let path = root.join("registry.json");
if path.exists() {
let current: DynamicNodeRegistry = read_json(&path, "REGISTRY")?;
if registry.registry_id != current.registry_id || registry.version <= current.version {
return Err("HOLOLAKE_DYNAMIC_REGISTRY_VERSION_NOT_INCREASING".into());
}
}
write_json_atomic(&path, &registry, "REGISTRY")?;
Ok(RegistryInstallReceipt {
schema: REGISTRY_SCHEMA,
state: "INSTALLED",
registry_id: registry.registry_id,
version: registry.version,
node_count: registry.nodes.len(),
})
}
pub(crate) fn record_health_at(
root: &Path,
health: SignedNodeHealth,
) -> Result<HealthRecordReceipt, String> {
validate_identifier(&health.node_id, "NODE")?;
let _lock = lock_file(
&root.join("health").join(format!("{}.lock", health.node_id)),
"NODE_HEALTH",
)?;
validate_capabilities(&health.capabilities)?;
if health.schema != HEALTH_SCHEMA || health.expires_at_unix_ms <= health.observed_at_unix_ms {
return Err("HOLOLAKE_NODE_HEALTH_INVALID".into());
}
let registry: DynamicNodeRegistry = read_json(&root.join("registry.json"), "REGISTRY")?;
if health.registry_version != registry.version {
return Err("HOLOLAKE_NODE_HEALTH_REGISTRY_VERSION_MISMATCH".into());
}
let node = registry
.nodes
.iter()
.find(|node| node.node_id == health.node_id)
.ok_or("HOLOLAKE_NODE_NOT_REGISTERED")?;
verify_signature(
&node.protocol_public_key,
&health.signature,
canonical_health_bytes(&health).as_bytes(),
"NODE_HEALTH",
)?;
let path = root.join("health").join(format!("{}.json", health.node_id));
if path.exists() {
let current: SignedNodeHealth = read_json(&path, "NODE_HEALTH")?;
if health.sequence <= current.sequence {
return Err("HOLOLAKE_NODE_HEALTH_REPLAYED".into());
}
}
write_json_atomic(&path, &health, "NODE_HEALTH")?;
Ok(HealthRecordReceipt {
schema: HEALTH_SCHEMA,
state: "RECORDED",
node_id: health.node_id,
sequence: health.sequence,
expires_at_unix_ms: health.expires_at_unix_ms,
})
}
pub(crate) fn resolve_at(
root: &Path,
input: ResolveCapabilityRouteInput,
now: u64,
) -> Result<CapabilityRouteReceipt, String> {
validate_identifier(&input.human_id, "HUMAN")?;
validate_identifier(&input.persona_id, "PERSONA")?;
validate_identifier(&input.domain_id, "DOMAIN")?;
validate_identifier(&input.capability_id, "CAPABILITY")?;
let registry: DynamicNodeRegistry = read_json(&root.join("registry.json"), "REGISTRY")?;
let mut eligible = Vec::new();
for node in registry.nodes.iter().filter(|node| {
node.enabled
&& node.domain_id == input.domain_id
&& node.human_ids.contains(&input.human_id)
&& node.persona_ids.contains(&input.persona_id)
&& node.capabilities.contains(&input.capability_id)
}) {
let path = root.join("health").join(format!("{}.json", node.node_id));
let Ok(health) = read_json::<SignedNodeHealth>(&path, "NODE_HEALTH") else {
continue;
};
if health.registry_version != registry.version
|| health.status != HealthStatus::Ready
|| health.observed_at_unix_ms > now.saturating_add(MAX_CLOCK_SKEW_MS)
|| health.expires_at_unix_ms < now
|| !health.capabilities.contains(&input.capability_id)
|| verify_signature(
&node.protocol_public_key,
&health.signature,
canonical_health_bytes(&health).as_bytes(),
"NODE_HEALTH",
)
.is_err()
{
continue;
}
eligible.push((node, health));
}
eligible.sort_by(
|(left, _), (right, _)| match left.priority.cmp(&right.priority) {
Ordering::Equal => left.node_id.cmp(&right.node_id),
ordering => ordering,
},
);
let (node, health) = eligible
.into_iter()
.next()
.ok_or("HOLOLAKE_NO_HEALTHY_CAPABILITY_ROUTE")?;
let receipt_id = sha256_hex(
format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
registry.registry_id,
registry.version,
node.node_id,
input.human_id,
input.persona_id,
input.domain_id,
input.capability_id,
health.sequence
)
.as_bytes(),
);
Ok(CapabilityRouteReceipt {
schema: ROUTE_SCHEMA,
state: "ROUTE_SELECTED",
registry_id: registry.registry_id,
registry_version: registry.version,
node_id: node.node_id.clone(),
domain_id: node.domain_id.clone(),
endpoint: node.endpoint.clone(),
capability_id: input.capability_id,
human_id: input.human_id,
persona_id: input.persona_id,
health_sequence: health.sequence,
health_expires_at_unix_ms: health.expires_at_unix_ms,
write_authority_granted: false,
receipt_id,
})
}
fn validate_registry(registry: &DynamicNodeRegistry) -> Result<(), String> {
if registry.schema != REGISTRY_SCHEMA
|| registry.version == 0
|| registry.nodes.is_empty()
|| registry.nodes.len() > MAX_NODES
{
return Err("HOLOLAKE_DYNAMIC_REGISTRY_INVALID".into());
}
validate_identifier(&registry.registry_id, "REGISTRY")?;
validate_identifier(&registry.signer_id, "SIGNER")?;
let mut node_ids = BTreeSet::new();
for node in &registry.nodes {
validate_identifier(&node.node_id, "NODE")?;
validate_identifier(&node.domain_id, "DOMAIN")?;
validate_identifiers(&node.human_ids, "HUMAN")?;
validate_identifiers(&node.persona_ids, "PERSONA")?;
validate_capabilities(&node.capabilities)?;
if !node_ids.insert(node.node_id.as_str()) {
return Err("HOLOLAKE_DYNAMIC_REGISTRY_DUPLICATE_NODE".into());
}
let endpoint = tauri::Url::parse(&node.endpoint)
.map_err(|error| format!("HOLOLAKE_NODE_ENDPOINT_INVALID: {error}"))?;
if endpoint.scheme() != "https"
|| endpoint.host_str().is_none()
|| !endpoint.username().is_empty()
|| endpoint.password().is_some()
|| endpoint.query().is_some()
|| endpoint.fragment().is_some()
{
return Err("HOLOLAKE_NODE_ENDPOINT_MUST_USE_HTTPS".into());
}
decode_exact(&node.protocol_public_key, 32, "NODE_PROTOCOL_PUBLIC_KEY")?;
}
Ok(())
}
fn validate_capabilities(values: &[String]) -> Result<(), String> {
validate_identifiers(values, "CAPABILITY")
}
fn validate_identifiers(values: &[String], kind: &str) -> Result<(), String> {
if values.is_empty() || values.len() > MAX_BINDINGS_PER_NODE {
return Err(format!("HOLOLAKE_{kind}_BINDING_REQUIRED"));
}
let mut unique = BTreeSet::new();
for value in values {
validate_identifier(value, kind)?;
if !unique.insert(value.as_str()) {
return Err(format!("HOLOLAKE_{kind}_BINDING_DUPLICATE"));
}
}
Ok(())
}
fn validate_identifier(value: &str, kind: &str) -> Result<(), String> {
if value.is_empty()
|| value.len() > MAX_IDENTIFIER_BYTES
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
{
return Err(format!("HOLOLAKE_{kind}_ID_INVALID"));
}
Ok(())
}
fn canonical_registry_bytes(registry: &DynamicNodeRegistry) -> String {
let mut lines = vec![
registry.schema.clone(),
registry.registry_id.clone(),
registry.version.to_string(),
registry.signer_id.clone(),
];
for node in &registry.nodes {
lines.extend([
node.node_id.clone(),
node.domain_id.clone(),
node.endpoint.clone(),
node.protocol_public_key.clone(),
node.human_ids.join(","),
node.persona_ids.join(","),
node.capabilities.join(","),
node.priority.to_string(),
node.enabled.to_string(),
]);
}
lines.join("\n")
}
fn canonical_health_bytes(health: &SignedNodeHealth) -> String {
[
health.schema.clone(),
health.registry_version.to_string(),
health.node_id.clone(),
health.observed_at_unix_ms.to_string(),
health.expires_at_unix_ms.to_string(),
health.sequence.to_string(),
match health.status {
HealthStatus::Ready => "READY",
HealthStatus::Degraded => "DEGRADED",
HealthStatus::Offline => "OFFLINE",
}
.into(),
health.capabilities.join(","),
]
.join("\n")
}
fn verify_signature(
public_key: &str,
signature: &str,
bytes: &[u8],
kind: &str,
) -> Result<(), String> {
let public_key = decode_exact(public_key, 32, &format!("{kind}_PUBLIC_KEY"))?;
let signature = decode_exact(signature, 64, &format!("{kind}_SIGNATURE"))?;
UnparsedPublicKey::new(&ED25519, public_key)
.verify(bytes, &signature)
.map_err(|_| format!("HOLOLAKE_{kind}_SIGNATURE_INVALID"))
}
fn trusted_registry_key(raw: &str, signer_id: &str) -> Result<String, String> {
let trust: DynamicRoutingTrust = serde_json::from_str(raw)
.map_err(|error| format!("HOLOLAKE_DYNAMIC_ROUTING_TRUST_INVALID: {error}"))?;
if trust.schema != "hololake.dynamic-routing-trust/v1" {
return Err("HOLOLAKE_DYNAMIC_ROUTING_TRUST_SCHEMA_UNSUPPORTED".into());
}
match trust.state {
RoutingTrustState::UnprovisionedFailClosed => {
if !trust.signers.is_empty() {
return Err("HOLOLAKE_DYNAMIC_ROUTING_TRUST_INVALID".into());
}
Err("HOLOLAKE_DYNAMIC_ROUTING_TRUST_UNPROVISIONED".into())
}
RoutingTrustState::Provisioned => {
if trust.signers.is_empty() {
return Err("HOLOLAKE_DYNAMIC_ROUTING_TRUST_INVALID".into());
}
let mut ids = BTreeSet::new();
for signer in trust.signers {
validate_identifier(&signer.signer_id, "SIGNER")?;
decode_exact(&signer.public_key, 32, "REGISTRY_PUBLIC_KEY")?;
if !ids.insert(signer.signer_id.clone()) {
return Err("HOLOLAKE_DYNAMIC_ROUTING_TRUST_DUPLICATE_SIGNER".into());
}
if signer.signer_id == signer_id {
return Ok(signer.public_key);
}
}
Err("HOLOLAKE_DYNAMIC_ROUTING_SIGNER_NOT_TRUSTED".into())
}
}
}
fn decode_exact(value: &str, expected: usize, kind: &str) -> Result<Vec<u8>, String> {
let decoded = BASE64
.decode(value)
.map_err(|_| format!("HOLOLAKE_{kind}_INVALID"))?;
if decoded.len() != expected {
return Err(format!("HOLOLAKE_{kind}_INVALID"));
}
Ok(decoded)
}
fn write_json_atomic<T: Serialize>(path: &Path, value: &T, kind: &str) -> Result<(), String> {
let parent = path
.parent()
.ok_or("HOLOLAKE_DYNAMIC_ROUTING_PATH_INVALID")?;
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_DYNAMIC_ROUTING_STORAGE_UNAVAILABLE: {error}"))?;
let temporary = parent.join(format!(".{}.tmp", Uuid::new_v4()));
let bytes = serde_json::to_vec_pretty(value)
.map_err(|error| format!("HOLOLAKE_{kind}_INVALID: {error}"))?;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
.map_err(|error| format!("HOLOLAKE_{kind}_WRITE_FAILED: {error}"))?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("HOLOLAKE_{kind}_WRITE_FAILED: {error}"))?;
fs::rename(&temporary, path).map_err(|error| format!("HOLOLAKE_{kind}_WRITE_FAILED: {error}"))
}
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path, kind: &str) -> Result<T, String> {
let bytes = fs::read(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
format!("HOLOLAKE_{kind}_NOT_FOUND")
} else {
format!("HOLOLAKE_{kind}_UNREADABLE: {error}")
}
})?;
serde_json::from_slice(&bytes).map_err(|error| format!("HOLOLAKE_{kind}_INVALID: {error}"))
}
fn lock_file(path: &Path, kind: &str) -> Result<std::fs::File, String> {
let parent = path
.parent()
.ok_or("HOLOLAKE_DYNAMIC_ROUTING_PATH_INVALID")?;
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_DYNAMIC_ROUTING_STORAGE_UNAVAILABLE: {error}"))?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.map_err(|error| format!("HOLOLAKE_{kind}_LOCK_FAILED: {error}"))?;
file.lock_exclusive()
.map_err(|error| format!("HOLOLAKE_{kind}_LOCK_FAILED: {error}"))?;
Ok(file)
}
fn sha256_hex(value: &[u8]) -> String {
ring::digest::digest(&ring::digest::SHA256, value)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use tempfile::TempDir;
fn key_pair() -> Ed25519KeyPair {
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
}
fn node(node_id: &str, key: &Ed25519KeyPair, priority: u32) -> DynamicNodeRecord {
DynamicNodeRecord {
node_id: node_id.into(),
domain_id: "DOM-FIFTH-0001".into(),
endpoint: format!("https://{node_id}.example.test/hololake"),
protocol_public_key: BASE64.encode(key.public_key().as_ref()),
human_ids: vec!["human-BS-0001".into()],
persona_ids: vec!["ICE-P-ZY001".into()],
capabilities: vec!["GH-PNCC-READ".into()],
priority,
enabled: true,
}
}
fn registry(nodes: Vec<DynamicNodeRecord>, signer: &Ed25519KeyPair) -> DynamicNodeRegistry {
let mut registry = DynamicNodeRegistry {
schema: REGISTRY_SCHEMA.into(),
registry_id: "registry-1".into(),
version: 1,
nodes,
signer_id: "jd-controller-release-registry".into(),
signature: String::new(),
};
registry.signature = BASE64.encode(
signer
.sign(canonical_registry_bytes(&registry).as_bytes())
.as_ref(),
);
registry
}
fn health(node_id: &str, signer: &Ed25519KeyPair, now: u64) -> SignedNodeHealth {
let mut health = SignedNodeHealth {
schema: HEALTH_SCHEMA.into(),
registry_version: 1,
node_id: node_id.into(),
observed_at_unix_ms: now,
expires_at_unix_ms: now + 60_000,
sequence: 1,
status: HealthStatus::Ready,
capabilities: vec!["GH-PNCC-READ".into()],
signature: String::new(),
};
health.signature = BASE64.encode(
signer
.sign(canonical_health_bytes(&health).as_bytes())
.as_ref(),
);
health
}
fn input() -> ResolveCapabilityRouteInput {
ResolveCapabilityRouteInput {
human_id: "human-BS-0001".into(),
persona_id: "ICE-P-ZY001".into(),
domain_id: "DOM-FIFTH-0001".into(),
capability_id: "GH-PNCC-READ".into(),
}
}
#[test]
fn installs_a_signed_dynamic_registry_and_selects_by_priority() {
let temp = TempDir::new().unwrap();
let registry_signer = key_pair();
let node_a = key_pair();
let node_b = key_pair();
let registry = registry(
vec![node("node-b", &node_b, 20), node("node-a", &node_a, 10)],
&registry_signer,
);
install_registry_at(
temp.path(),
registry,
&BASE64.encode(registry_signer.public_key().as_ref()),
)
.unwrap();
let now = 1_000_000;
record_health_at(temp.path(), health("node-a", &node_a, now)).unwrap();
record_health_at(temp.path(), health("node-b", &node_b, now)).unwrap();
let route = resolve_at(temp.path(), input(), now + 1).unwrap();
assert_eq!(route.node_id, "node-a");
assert!(!route.write_authority_granted);
}
#[test]
fn rejects_unsigned_registry_and_replayed_health() {
let temp = TempDir::new().unwrap();
let registry_signer = key_pair();
let node_key = key_pair();
let mut invalid = registry(vec![node("node-a", &node_key, 10)], &registry_signer);
invalid.signature = BASE64.encode([0_u8; 64]);
assert!(install_registry_at(
temp.path(),
invalid,
&BASE64.encode(registry_signer.public_key().as_ref())
)
.is_err());
install_registry_at(
temp.path(),
registry(vec![node("node-a", &node_key, 10)], &registry_signer),
&BASE64.encode(registry_signer.public_key().as_ref()),
)
.unwrap();
let current = health("node-a", &node_key, 1_000_000);
record_health_at(temp.path(), current.clone()).unwrap();
assert_eq!(
record_health_at(temp.path(), current).unwrap_err(),
"HOLOLAKE_NODE_HEALTH_REPLAYED"
);
}
#[test]
fn stale_unsigned_or_wrong_binding_nodes_are_not_routable() {
let temp = TempDir::new().unwrap();
let registry_signer = key_pair();
let node_key = key_pair();
install_registry_at(
temp.path(),
registry(vec![node("node-a", &node_key, 10)], &registry_signer),
&BASE64.encode(registry_signer.public_key().as_ref()),
)
.unwrap();
record_health_at(temp.path(), health("node-a", &node_key, 1_000_000)).unwrap();
assert_eq!(
resolve_at(temp.path(), input(), 2_000_000).unwrap_err(),
"HOLOLAKE_NO_HEALTHY_CAPABILITY_ROUTE"
);
let mut wrong_persona = input();
wrong_persona.persona_id = "ICE-P-OTHER".into();
assert_eq!(
resolve_at(temp.path(), wrong_persona, 1_000_001).unwrap_err(),
"HOLOLAKE_NO_HEALTHY_CAPABILITY_ROUTE"
);
}
#[test]
fn embedded_unprovisioned_trust_fails_closed() {
assert_eq!(
trusted_registry_key(EMBEDDED_ROUTING_TRUST, "any-signer").unwrap_err(),
"HOLOLAKE_DYNAMIC_ROUTING_TRUST_UNPROVISIONED"
);
}
}

View file

@ -1,5 +1,6 @@
mod direct_local_broker; mod direct_local_broker;
mod direct_local_session; mod direct_local_session;
mod dynamic_capability_routing;
mod local_development_bridge; mod local_development_bridge;
mod release_trust; mod release_trust;