feat: expose HoloLake development lane to authenticated carriers
This commit is contained in:
parent
3294447dce
commit
96769846ac
7 changed files with 324 additions and 9 deletions
|
|
@ -1,14 +1,20 @@
|
|||
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
|
||||
use crate::direct_local_session::{
|
||||
append_event_at, authenticate_privileged_at, direct_session_root, issue_ticket_at, open_at,
|
||||
resume_at, AppendSessionEventInput, AuthenticateSessionInput, DirectSessionReceipt,
|
||||
IssueDiscoveryTicketInput, OpenSessionInput, ResumeSessionInput,
|
||||
append_event_at, authenticate_context_at, authenticate_privileged_at, direct_session_root,
|
||||
issue_ticket_at, open_at, resume_at, AppendSessionEventInput, AuthenticateSessionInput,
|
||||
AuthenticatedSessionContext, DirectSessionReceipt, IssueDiscoveryTicketInput, 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 crate::local_development_bridge::{
|
||||
account_key_for as development_account_key_for, acquire_at as acquire_development_lane_at,
|
||||
inspect_at as inspect_development_lane_at, release_at as release_development_lane_at,
|
||||
AcquireWriteLaneInput, ReleaseWriteLaneInput,
|
||||
};
|
||||
use crate::persona_time_authority::{
|
||||
beijing_time_coordinate, issue_authenticated_at, IssuePersonaTimeTicketInput,
|
||||
};
|
||||
|
|
@ -119,6 +125,7 @@ struct BrokerStorageRoots {
|
|||
pncc_projection: PathBuf,
|
||||
language_inbox: PathBuf,
|
||||
persona_time: PathBuf,
|
||||
development: PathBuf,
|
||||
}
|
||||
|
||||
impl Drop for DirectLocalBrokerHandle {
|
||||
|
|
@ -154,6 +161,9 @@ enum BrokerRequest {
|
|||
QueryPnccReceiptProjection(AuthenticatedPnccProjectionQueryInput),
|
||||
GetBeijingTime,
|
||||
IssuePersonaTimeTicket(IssuePersonaTimeTicketInput),
|
||||
AcquireDevelopmentWriteLane(AuthenticatedDevelopmentLaneInput),
|
||||
InspectDevelopmentWriteLane(AuthenticatedDevelopmentInspectInput),
|
||||
ReleaseDevelopmentWriteLane(AuthenticatedDevelopmentReleaseInput),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
@ -232,6 +242,27 @@ struct AuthenticatedPnccProjectionQueryInput {
|
|||
query: QueryPnccReceiptProjectionInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AuthenticatedDevelopmentLaneInput {
|
||||
session: AuthenticateSessionInput,
|
||||
lane: AcquireWriteLaneInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AuthenticatedDevelopmentInspectInput {
|
||||
session: AuthenticateSessionInput,
|
||||
account_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
struct AuthenticatedDevelopmentReleaseInput {
|
||||
session: AuthenticateSessionInput,
|
||||
lane: ReleaseWriteLaneInput,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BrokerResponse {
|
||||
|
|
@ -408,6 +439,9 @@ fn start_at(
|
|||
.parent()
|
||||
.ok_or("HOLOLAKE_BROKER_PERSONA_TIME_STORAGE_BOUNDARY_INVALID")?
|
||||
.join("persona-time-authority-v1");
|
||||
let development_root = development_root_from_session_root(&session_root)?;
|
||||
fs::create_dir_all(&development_root)
|
||||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_DIR_FAILED: {error}"))?;
|
||||
|
|
@ -463,6 +497,7 @@ fn start_at(
|
|||
pncc_projection: pncc_projection_root,
|
||||
language_inbox: language_inbox_root,
|
||||
persona_time: persona_time_root,
|
||||
development: development_root,
|
||||
},
|
||||
&worker_shutdown,
|
||||
&worker_authenticated_connections,
|
||||
|
|
@ -586,6 +621,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
let pncc_projection_root = &roots.pncc_projection;
|
||||
let language_inbox_root = &roots.language_inbox;
|
||||
let persona_time_root = &roots.persona_time;
|
||||
let development_root = &roots.development;
|
||||
let request: BrokerRequest = match serde_json::from_slice(bytes) {
|
||||
Ok(request) => request,
|
||||
Err(error) => {
|
||||
|
|
@ -682,6 +718,41 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
issue_authenticated_at(persona_time_root, session_root, input)
|
||||
.and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string()))
|
||||
}
|
||||
BrokerRequest::AcquireDevelopmentWriteLane(input) => {
|
||||
authenticate_development_request(session_root, &input.session, &input.lane.account_id)
|
||||
.and_then(|context| {
|
||||
if context.lane_id != input.lane.lane_id
|
||||
|| context.client_instance_id != input.lane.owner_instance_id
|
||||
{
|
||||
return Err("HOLOLAKE_DEVELOPMENT_LANE_SESSION_MISMATCH".into());
|
||||
}
|
||||
acquire_development_lane_at(development_root, input.lane)
|
||||
})
|
||||
.and_then(|receipt| {
|
||||
serde_json::to_value(receipt).map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
BrokerRequest::InspectDevelopmentWriteLane(input) => {
|
||||
authenticate_development_request(session_root, &input.session, &input.account_id)
|
||||
.and_then(|_| inspect_development_lane_at(development_root, &input.account_id))
|
||||
.and_then(|receipt| {
|
||||
serde_json::to_value(receipt).map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
BrokerRequest::ReleaseDevelopmentWriteLane(input) => {
|
||||
authenticate_development_request(session_root, &input.session, &input.lane.account_id)
|
||||
.and_then(|context| {
|
||||
if context.lane_id != input.lane.lane_id
|
||||
|| context.client_instance_id != input.lane.owner_instance_id
|
||||
{
|
||||
return Err("HOLOLAKE_DEVELOPMENT_LANE_SESSION_MISMATCH".into());
|
||||
}
|
||||
release_development_lane_at(development_root, input.lane)
|
||||
})
|
||||
.and_then(|receipt| {
|
||||
serde_json::to_value(receipt).map_err(|error| error.to_string())
|
||||
})
|
||||
}
|
||||
};
|
||||
match result {
|
||||
Ok(value) => BrokerResponse::success(value),
|
||||
|
|
@ -689,6 +760,32 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
|||
}
|
||||
}
|
||||
|
||||
fn authenticate_development_request(
|
||||
session_root: &Path,
|
||||
session: &AuthenticateSessionInput,
|
||||
account_id: &str,
|
||||
) -> Result<AuthenticatedSessionContext, String> {
|
||||
let context = authenticate_context_at(session_root, session)?;
|
||||
if context.account_key != development_account_key_for(account_id)? {
|
||||
return Err("HOLOLAKE_DEVELOPMENT_ACCOUNT_SESSION_MISMATCH".into());
|
||||
}
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
fn development_root_from_session_root(session_root: &Path) -> Result<PathBuf, String> {
|
||||
let account_root = session_root
|
||||
.parent()
|
||||
.ok_or("HOLOLAKE_BROKER_DEVELOPMENT_STORAGE_BOUNDARY_INVALID")?;
|
||||
if session_root.file_name().and_then(|name| name.to_str()) == Some("direct-local-session-v1") {
|
||||
let app_data = account_root
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or("HOLOLAKE_BROKER_DEVELOPMENT_STORAGE_BOUNDARY_INVALID")?;
|
||||
return Ok(app_data.join("local-development-bridge-v1"));
|
||||
}
|
||||
Ok(account_root.join("local-development-bridge-v1"))
|
||||
}
|
||||
|
||||
fn open_visitor_session_at(
|
||||
session_root: &Path,
|
||||
input: OpenVisitorSessionInput,
|
||||
|
|
@ -1152,6 +1249,157 @@ mod tests {
|
|||
assert_eq!(verified["result"]["returnedEventCount"], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_connector_can_acquire_and_inspect_the_hololake_development_lane() {
|
||||
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("accounts-v1/test-account/direct-local-session-v1");
|
||||
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: "hololake-zero-core-runtime-20260817".into(),
|
||||
client_instance_id: "codex-1".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let opened = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "OPEN_SESSION",
|
||||
"input": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "hololake-zero-core-runtime-20260817",
|
||||
"clientInstanceId": "codex-1",
|
||||
"discoveryTicket": ticket.discovery_ticket
|
||||
}
|
||||
}),
|
||||
);
|
||||
let session = serde_json::json!({
|
||||
"accountId": "human-1",
|
||||
"sessionId": opened["result"]["sessionId"],
|
||||
"resumeSecret": opened["result"]["resumeSecret"]
|
||||
});
|
||||
|
||||
let acquired = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "ACQUIRE_DEVELOPMENT_WRITE_LANE",
|
||||
"input": {
|
||||
"session": session,
|
||||
"lane": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "hololake-zero-core-runtime-20260817",
|
||||
"ownerInstanceId": "codex-1",
|
||||
"resumeToken": null
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(acquired["ok"], true);
|
||||
assert_eq!(acquired["result"]["state"], "ACQUIRED");
|
||||
|
||||
let inspected = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "INSPECT_DEVELOPMENT_WRITE_LANE",
|
||||
"input": {
|
||||
"session": session,
|
||||
"accountId": "human-1"
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(inspected["ok"], true);
|
||||
assert_eq!(inspected["result"]["state"], "ACTIVE");
|
||||
assert_eq!(
|
||||
inspected["result"]["laneId"],
|
||||
"hololake-zero-core-runtime-20260817"
|
||||
);
|
||||
assert_eq!(inspected["result"]["ownerInstanceId"], "codex-1");
|
||||
|
||||
let released = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "RELEASE_DEVELOPMENT_WRITE_LANE",
|
||||
"input": {
|
||||
"session": session,
|
||||
"lane": {
|
||||
"accountId": "human-1",
|
||||
"laneId": "hololake-zero-core-runtime-20260817",
|
||||
"ownerInstanceId": "codex-1",
|
||||
"resumeToken": acquired["result"]["resumeToken"]
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(released["ok"], true);
|
||||
assert_eq!(released["result"]["state"], "RELEASED");
|
||||
|
||||
let available = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "INSPECT_DEVELOPMENT_WRITE_LANE",
|
||||
"input": {
|
||||
"session": session,
|
||||
"accountId": "human-1"
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(available["result"]["state"], "AVAILABLE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expression_only_visitor_cannot_acquire_a_development_lane() {
|
||||
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("accounts-v1/test-account/direct-local-session-v1");
|
||||
let routes = temp.path().join("routes");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
fs::create_dir_all(&routes).unwrap();
|
||||
let _broker = start_at(sessions, routes, descriptor, socket.clone()).unwrap();
|
||||
let opened = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "OPEN_VISITOR_SESSION",
|
||||
"input": {"clientInstanceId": "generic-ai-1"}
|
||||
}),
|
||||
);
|
||||
let acquired = request(
|
||||
&socket,
|
||||
serde_json::json!({
|
||||
"operation": "ACQUIRE_DEVELOPMENT_WRITE_LANE",
|
||||
"input": {
|
||||
"session": {
|
||||
"accountId": opened["result"]["accountId"],
|
||||
"sessionId": opened["result"]["session"]["sessionId"],
|
||||
"resumeSecret": opened["result"]["session"]["resumeSecret"]
|
||||
},
|
||||
"lane": {
|
||||
"accountId": opened["result"]["accountId"],
|
||||
"laneId": "unauthorized-development",
|
||||
"ownerInstanceId": "generic-ai-1",
|
||||
"resumeToken": null
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert_eq!(acquired["ok"], false);
|
||||
assert_eq!(
|
||||
acquired["error"],
|
||||
"HOLOLAKE_VISITOR_SESSION_HAS_NO_SYSTEM_AUTHORITY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_authenticated_persistent_connector_counts_as_online() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -116,7 +116,10 @@ fn bridge_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
.map_err(|error| format!("HOLOLAKE_BRIDGE_STORAGE_UNAVAILABLE: {error}"))
|
||||
}
|
||||
|
||||
fn acquire_at(root: &Path, input: AcquireWriteLaneInput) -> Result<WriteLaneReceipt, String> {
|
||||
pub(crate) 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")?;
|
||||
|
|
@ -186,7 +189,7 @@ fn resume_existing(
|
|||
Ok(receipt_for("RESUMED", &record, None, observed_at))
|
||||
}
|
||||
|
||||
fn inspect_at(root: &Path, account_id: &str) -> Result<InspectWriteLaneReceipt, String> {
|
||||
pub(crate) 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
|
||||
|
|
@ -217,7 +220,10 @@ fn inspect_at(root: &Path, account_id: &str) -> Result<InspectWriteLaneReceipt,
|
|||
})
|
||||
}
|
||||
|
||||
fn release_at(root: &Path, input: ReleaseWriteLaneInput) -> Result<WriteLaneReceipt, String> {
|
||||
pub(crate) 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")?;
|
||||
|
|
@ -339,6 +345,11 @@ fn sha256_hex(value: &[u8]) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn account_key_for(account_id: &str) -> Result<String, String> {
|
||||
validate_identifier(account_id, "ACCOUNT")?;
|
||||
Ok(sha256_hex(account_id.as_bytes()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue