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
|
|
@ -112,12 +112,17 @@
|
||||||
"connector_capability_bootstrap_runtime": true,
|
"connector_capability_bootstrap_runtime": true,
|
||||||
"installed_app_connector_entry_runtime": true,
|
"installed_app_connector_entry_runtime": true,
|
||||||
"external_local_broker_runtime": true,
|
"external_local_broker_runtime": true,
|
||||||
|
"authenticated_broker_development_lane_runtime": true,
|
||||||
|
"development_lane_human_projection_runtime": true,
|
||||||
|
"visitor_development_lane_rejected": true,
|
||||||
"resumable_direct_session_kernel_runtime": true,
|
"resumable_direct_session_kernel_runtime": true,
|
||||||
"single_use_discovery_ticket_runtime": true,
|
"single_use_discovery_ticket_runtime": true,
|
||||||
"hololake_issued_discovery_ticket_runtime": true,
|
"hololake_issued_discovery_ticket_runtime": true,
|
||||||
"cross_process_session_event_lock_runtime": true,
|
"cross_process_session_event_lock_runtime": true,
|
||||||
"direct_session_account_single_writer_runtime": true,
|
"direct_session_account_single_writer_runtime": true,
|
||||||
"idempotent_session_event_cursor_runtime": true,
|
"idempotent_session_event_cursor_runtime": true,
|
||||||
"incremental_repository_channel_migrated_to_native_mainline": false
|
"incremental_repository_channel_migrated_to_native_mainline": false,
|
||||||
|
"native_general_programming_tool_loop_runtime": false,
|
||||||
|
"supervised_shell_execution_runtime": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ The visible shell is HoloLake itself. Git is the durable history engine below it
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
An authenticated non-visitor connector may now acquire, inspect and explicitly release the existing account-scoped development write lane through that broker. Account, lane and client instance must match the HoloLake session before the bridge mutates. HoloLake projects the same Rust-owned lane state on the system-details page, so a human can distinguish a nearby expression-only visitor from an active development writer. This is a controlled writer handoff, not a general programming tool loop: shell, file patching, build execution, publication and deployment still require later supervised execution organs and separate authorization receipts.
|
||||||
|
|
||||||
## Circular-lake protocol membrane and nearby AI
|
## Circular-lake protocol membrane and nearby AI
|
||||||
|
|
||||||
HoloLake 0.4.0 places a deterministic protocol membrane in front of the local language inbox. The membrane accepts only strict GLP/1.0 expression envelopes from a HoloLake-issued visitor session. Unknown fields, malformed identifiers, incorrect checksums, oversized content, attachments and command content are rejected before storage. Accepted natural language is an expression receipt only; it never carries execution authority by itself. Intent interpretation remains behind the membrane and cannot weaken its structural admission rules.
|
HoloLake 0.4.0 places a deterministic protocol membrane in front of the local language inbox. The membrane accepts only strict GLP/1.0 expression envelopes from a HoloLake-issued visitor session. Unknown fields, malformed identifiers, incorrect checksums, oversized content, attachments and command content are rejected before storage. Accepted natural language is an expression receipt only; it never carries execution authority by itself. Intent interpretation remains behind the membrane and cannot weaken its structural admission rules.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# ADR 0005: Authenticated development lane projection
|
||||||
|
|
||||||
|
- Status: accepted for the next HoloLake desktop candidate
|
||||||
|
- Date: 2026-08-17
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The same-device HoloLake broker can already discover an external AI, open an expression-only visitor session, and resume a HoloLake-issued authenticated session. The local development bridge can already enforce one writer per account, but it is reachable only from the WebView command surface. As a result, an external programming carrier can be visibly connected while still being unable to acquire the HoloLake-owned development lane. The system page also cannot distinguish a connected visitor from an active development writer.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Expose acquire, inspect, and release operations for the existing local development lane through the user-only Unix broker. Every operation requires a non-visitor HoloLake session. The session account must equal the lane account, the session lane must equal the requested write lane, and the session client instance must equal the requested writer instance. A generic expression-only visitor is rejected before any lane mutation.
|
||||||
|
|
||||||
|
Project the active lane and writer on the HoloLake system-details page by reading the same Rust-owned bridge state. The projection does not create authority and is not a second state store.
|
||||||
|
|
||||||
|
This slice establishes the controlled writer handoff only. It does not yet provide a general shell, file mutation, patch, build, deployment, model, persona binding, or reality-execution engine.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The user needs to see whether development is merely connected or has actually switched into HoloLake's single-writer environment. Reusing the existing session and writer kernels closes that gap without turning socket discovery, MCP, or a visitor message into execution authority.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
- Letting any same-device visitor acquire a write lane: discovery and expression are not authorization.
|
||||||
|
- Maintaining a separate UI-only development status: it would create a second truth source.
|
||||||
|
- Calling the lane handoff a complete native development container: the programming tool loop and supervised execution engine remain unimplemented.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `src-tauri/src/direct_local_broker.rs`
|
||||||
|
- `src-tauri/src/direct_local_session.rs`
|
||||||
|
- `src-tauri/src/local_development_bridge.rs`
|
||||||
|
- `src/main.tsx`
|
||||||
|
- `contracts/local-development-bridge.json`
|
||||||
|
|
@ -27,6 +27,10 @@ test('one account has one writer while readers and reviewers can remain parallel
|
||||||
assert.equal(bridge.account_concurrency.parallel_readers_allowed, true)
|
assert.equal(bridge.account_concurrency.parallel_readers_allowed, true)
|
||||||
assert.equal(bridge.account_concurrency.parallel_reviewers_allowed, true)
|
assert.equal(bridge.account_concurrency.parallel_reviewers_allowed, true)
|
||||||
assert.equal(bridge.account_concurrency.parallel_editors_allowed, false)
|
assert.equal(bridge.account_concurrency.parallel_editors_allowed, false)
|
||||||
|
assert.equal(bridge.implementation.authenticated_broker_development_lane_runtime, true)
|
||||||
|
assert.equal(bridge.implementation.development_lane_human_projection_runtime, true)
|
||||||
|
assert.equal(bridge.implementation.visitor_development_lane_rejected, true)
|
||||||
|
assert.equal(bridge.implementation.native_general_programming_tool_loop_runtime, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('language-space reads are resumable pages rather than silent truncation', () => {
|
test('language-space reads are resumable pages rather than silent truncation', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
|
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
|
||||||
use crate::direct_local_session::{
|
use crate::direct_local_session::{
|
||||||
append_event_at, authenticate_privileged_at, direct_session_root, issue_ticket_at, open_at,
|
append_event_at, authenticate_context_at, authenticate_privileged_at, direct_session_root,
|
||||||
resume_at, AppendSessionEventInput, AuthenticateSessionInput, DirectSessionReceipt,
|
issue_ticket_at, open_at, resume_at, AppendSessionEventInput, AuthenticateSessionInput,
|
||||||
IssueDiscoveryTicketInput, OpenSessionInput, ResumeSessionInput,
|
AuthenticatedSessionContext, DirectSessionReceipt, IssueDiscoveryTicketInput, OpenSessionInput,
|
||||||
|
ResumeSessionInput,
|
||||||
};
|
};
|
||||||
use crate::dynamic_capability_routing::{
|
use crate::dynamic_capability_routing::{
|
||||||
install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at,
|
install_trusted_registry_at, record_health_at, resolve_at as resolve_capability_route_at,
|
||||||
routing_root as dynamic_routing_root, DynamicNodeRegistry, ResolveCapabilityRouteInput,
|
routing_root as dynamic_routing_root, DynamicNodeRegistry, ResolveCapabilityRouteInput,
|
||||||
SignedNodeHealth,
|
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::{
|
use crate::persona_time_authority::{
|
||||||
beijing_time_coordinate, issue_authenticated_at, IssuePersonaTimeTicketInput,
|
beijing_time_coordinate, issue_authenticated_at, IssuePersonaTimeTicketInput,
|
||||||
};
|
};
|
||||||
|
|
@ -119,6 +125,7 @@ struct BrokerStorageRoots {
|
||||||
pncc_projection: PathBuf,
|
pncc_projection: PathBuf,
|
||||||
language_inbox: PathBuf,
|
language_inbox: PathBuf,
|
||||||
persona_time: PathBuf,
|
persona_time: PathBuf,
|
||||||
|
development: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for DirectLocalBrokerHandle {
|
impl Drop for DirectLocalBrokerHandle {
|
||||||
|
|
@ -154,6 +161,9 @@ enum BrokerRequest {
|
||||||
QueryPnccReceiptProjection(AuthenticatedPnccProjectionQueryInput),
|
QueryPnccReceiptProjection(AuthenticatedPnccProjectionQueryInput),
|
||||||
GetBeijingTime,
|
GetBeijingTime,
|
||||||
IssuePersonaTimeTicket(IssuePersonaTimeTicketInput),
|
IssuePersonaTimeTicket(IssuePersonaTimeTicketInput),
|
||||||
|
AcquireDevelopmentWriteLane(AuthenticatedDevelopmentLaneInput),
|
||||||
|
InspectDevelopmentWriteLane(AuthenticatedDevelopmentInspectInput),
|
||||||
|
ReleaseDevelopmentWriteLane(AuthenticatedDevelopmentReleaseInput),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
|
@ -232,6 +242,27 @@ struct AuthenticatedPnccProjectionQueryInput {
|
||||||
query: QueryPnccReceiptProjectionInput,
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct BrokerResponse {
|
struct BrokerResponse {
|
||||||
|
|
@ -408,6 +439,9 @@ fn start_at(
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or("HOLOLAKE_BROKER_PERSONA_TIME_STORAGE_BOUNDARY_INVALID")?
|
.ok_or("HOLOLAKE_BROKER_PERSONA_TIME_STORAGE_BOUNDARY_INVALID")?
|
||||||
.join("persona-time-authority-v1");
|
.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() {
|
if let Some(parent) = socket_path.parent() {
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent)
|
||||||
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_DIR_FAILED: {error}"))?;
|
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_DIR_FAILED: {error}"))?;
|
||||||
|
|
@ -463,6 +497,7 @@ fn start_at(
|
||||||
pncc_projection: pncc_projection_root,
|
pncc_projection: pncc_projection_root,
|
||||||
language_inbox: language_inbox_root,
|
language_inbox: language_inbox_root,
|
||||||
persona_time: persona_time_root,
|
persona_time: persona_time_root,
|
||||||
|
development: development_root,
|
||||||
},
|
},
|
||||||
&worker_shutdown,
|
&worker_shutdown,
|
||||||
&worker_authenticated_connections,
|
&worker_authenticated_connections,
|
||||||
|
|
@ -586,6 +621,7 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
||||||
let pncc_projection_root = &roots.pncc_projection;
|
let pncc_projection_root = &roots.pncc_projection;
|
||||||
let language_inbox_root = &roots.language_inbox;
|
let language_inbox_root = &roots.language_inbox;
|
||||||
let persona_time_root = &roots.persona_time;
|
let persona_time_root = &roots.persona_time;
|
||||||
|
let development_root = &roots.development;
|
||||||
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) => {
|
||||||
|
|
@ -682,6 +718,41 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
|
||||||
issue_authenticated_at(persona_time_root, session_root, input)
|
issue_authenticated_at(persona_time_root, session_root, input)
|
||||||
.and_then(|ticket| serde_json::to_value(ticket).map_err(|error| error.to_string()))
|
.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 {
|
match result {
|
||||||
Ok(value) => BrokerResponse::success(value),
|
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(
|
fn open_visitor_session_at(
|
||||||
session_root: &Path,
|
session_root: &Path,
|
||||||
input: OpenVisitorSessionInput,
|
input: OpenVisitorSessionInput,
|
||||||
|
|
@ -1152,6 +1249,157 @@ mod tests {
|
||||||
assert_eq!(verified["result"]["returnedEventCount"], 0);
|
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]
|
#[test]
|
||||||
fn only_an_authenticated_persistent_connector_counts_as_online() {
|
fn only_an_authenticated_persistent_connector_counts_as_online() {
|
||||||
let temp = TempDir::new().unwrap();
|
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}"))
|
.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.account_id, "ACCOUNT")?;
|
||||||
validate_identifier(&input.lane_id, "LANE")?;
|
validate_identifier(&input.lane_id, "LANE")?;
|
||||||
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
||||||
|
|
@ -186,7 +189,7 @@ fn resume_existing(
|
||||||
Ok(receipt_for("RESUMED", &record, None, observed_at))
|
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")?;
|
validate_identifier(account_id, "ACCOUNT")?;
|
||||||
let account_key = sha256_hex(account_id.as_bytes());
|
let account_key = sha256_hex(account_id.as_bytes());
|
||||||
let active_path = root
|
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.account_id, "ACCOUNT")?;
|
||||||
validate_identifier(&input.lane_id, "LANE")?;
|
validate_identifier(&input.lane_id, "LANE")?;
|
||||||
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
validate_identifier(&input.owner_instance_id, "OWNER")?;
|
||||||
|
|
@ -339,6 +345,11 @@ fn sha256_hex(value: &[u8]) -> String {
|
||||||
.collect()
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,13 @@ interface HomeStatus {
|
||||||
releaseRecoveryState: string
|
releaseRecoveryState: string
|
||||||
mcpRole: string
|
mcpRole: string
|
||||||
}
|
}
|
||||||
|
interface DevelopmentWriteLaneStatus {
|
||||||
|
state: 'AVAILABLE' | 'ACTIVE'
|
||||||
|
laneId?: string
|
||||||
|
ownerInstanceId?: string
|
||||||
|
acquiredAtUnixMs?: number
|
||||||
|
observedAtUnixMs: number
|
||||||
|
}
|
||||||
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number }
|
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number }
|
||||||
interface PersonalChannelModule { moduleId: string; kind: string; displayName: string; state: string; installedAtUnixMs: number; timeZone: string; calendarName: string; clockVerification: string }
|
interface PersonalChannelModule { moduleId: string; kind: string; displayName: string; state: string; installedAtUnixMs: number; timeZone: string; calendarName: string; clockVerification: string }
|
||||||
interface LoginSession { username: string; host: string; domain: string; signedInAtUnixMs: number }
|
interface LoginSession { username: string; host: string; domain: string; signedInAtUnixMs: number }
|
||||||
|
|
@ -573,6 +580,7 @@ function HoloLakeApp() {
|
||||||
const [serverPncc, setServerPncc] = useState<ServerPnccProjection | null>(null)
|
const [serverPncc, setServerPncc] = useState<ServerPnccProjection | null>(null)
|
||||||
const [serverPnccBusy, setServerPnccBusy] = useState(false)
|
const [serverPnccBusy, setServerPnccBusy] = useState(false)
|
||||||
const [serverPnccReadback, setServerPnccReadback] = useState<'WAITING' | 'LIVE' | 'UNAVAILABLE'>('WAITING')
|
const [serverPnccReadback, setServerPnccReadback] = useState<'WAITING' | 'LIVE' | 'UNAVAILABLE'>('WAITING')
|
||||||
|
const [developmentLane, setDevelopmentLane] = useState<DevelopmentWriteLaneStatus | null>(null)
|
||||||
const [enterpriseEntry, setEnterpriseEntry] = useState<EnterpriseEntry | null>(null)
|
const [enterpriseEntry, setEnterpriseEntry] = useState<EnterpriseEntry | null>(null)
|
||||||
const [enterpriseEntryBusy, setEnterpriseEntryBusy] = useState(false)
|
const [enterpriseEntryBusy, setEnterpriseEntryBusy] = useState(false)
|
||||||
const [enterpriseReceiptBusy, setEnterpriseReceiptBusy] = useState(false)
|
const [enterpriseReceiptBusy, setEnterpriseReceiptBusy] = useState(false)
|
||||||
|
|
@ -600,12 +608,14 @@ function HoloLakeApp() {
|
||||||
const startup = await invoke<PersonaTimeAuthorityStartup>('start_persona_time_authority')
|
const startup = await invoke<PersonaTimeAuthorityStartup>('start_persona_time_authority')
|
||||||
setBeijingCoordinate(startup.coordinate)
|
setBeijingCoordinate(startup.coordinate)
|
||||||
} catch { /* 联网失败时其余首页能力仍可启动。 */ }
|
} catch { /* 联网失败时其余首页能力仍可启动。 */ }
|
||||||
const [homeResult, personalResult, knowledgeResult, codeResult, eraResult] = await Promise.allSettled([
|
const localAccountId = getOrCreateLocalId('hololake-local-account', 'human-local')
|
||||||
|
const [homeResult, personalResult, knowledgeResult, codeResult, eraResult, developmentResult] = await Promise.allSettled([
|
||||||
invoke<HomeStatus>('get_hololake_home_status'),
|
invoke<HomeStatus>('get_hololake_home_status'),
|
||||||
invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'),
|
invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'),
|
||||||
invoke<KnowledgeSnapshot>('get_knowledge_snapshot'),
|
invoke<KnowledgeSnapshot>('get_knowledge_snapshot'),
|
||||||
invoke<CodeChannelSnapshot>('get_code_channel_snapshot'),
|
invoke<CodeChannelSnapshot>('get_code_channel_snapshot'),
|
||||||
invoke<GuanghuEraTimeline>('get_guanghu_era_timeline'),
|
invoke<GuanghuEraTimeline>('get_guanghu_era_timeline'),
|
||||||
|
invoke<DevelopmentWriteLaneStatus>('inspect_development_write_lane', { input: { accountId: localAccountId } }),
|
||||||
])
|
])
|
||||||
if (homeResult.status === 'fulfilled') setStatus(homeResult.value)
|
if (homeResult.status === 'fulfilled') setStatus(homeResult.value)
|
||||||
if (personalResult.status === 'fulfilled') setPersonal(personalResult.value)
|
if (personalResult.status === 'fulfilled') setPersonal(personalResult.value)
|
||||||
|
|
@ -615,6 +625,7 @@ function HoloLakeApp() {
|
||||||
setEraTimeline(eraResult.value)
|
setEraTimeline(eraResult.value)
|
||||||
setBeijingCoordinate(eraResult.value.currentCoordinate)
|
setBeijingCoordinate(eraResult.value.currentCoordinate)
|
||||||
}
|
}
|
||||||
|
setDevelopmentLane(developmentResult.status === 'fulfilled' ? developmentResult.value : null)
|
||||||
}, [])
|
}, [])
|
||||||
const loadReceipts = useCallback(async () => {
|
const loadReceipts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1321,7 +1332,7 @@ function HoloLakeApp() {
|
||||||
</dl> : <p className="boundary-note">当前无法取得京东主控的实时回读。界面不会以静态记录替代在线状态。</p>}
|
</dl> : <p className="boundary-note">当前无法取得京东主控的实时回读。界面不会以静态记录替代在线状态。</p>}
|
||||||
<button className="secondary-button" type="button" disabled={serverPnccBusy} onClick={() => void refreshServerPncc()}>{serverPnccBusy ? '正在读取…' : '重新读取主控状态'}</button>
|
<button className="secondary-button" type="button" disabled={serverPnccBusy} onClick={() => void refreshServerPncc()}>{serverPnccBusy ? '正在读取…' : '重新读取主控状态'}</button>
|
||||||
</section>
|
</section>
|
||||||
<section className="plain-panel"><header><div><h2>本机 GH-PNCC 投影</h2><p>本机仓库挂载与读取回执来自同一运行事件流。</p></div></header><dl className="evidence-list"><div><dt>已挂载仓库</dt><dd>{status.codeRepositoryMountCount}</dd></div><div><dt>投影回执</dt><dd>{status.pnccReceiptCount}</dd></div><div><dt>当前结论</dt><dd>{status.codeRepositoryMountCount > 0 && status.pnccReceiptCount > 0 ? '已有可核验运行记录' : '接口已接入,尚无完整运行记录'}</dd></div></dl></section>
|
<section className="plain-panel"><header><div><h2>本机 GH-PNCC 投影</h2><p>本机仓库挂载、开发写入通道与读取回执来自同一运行事件流。</p></div></header><dl className="evidence-list"><div><dt>已挂载仓库</dt><dd>{status.codeRepositoryMountCount}</dd></div><div><dt>投影回执</dt><dd>{status.pnccReceiptCount}</dd></div><div><dt>开发执行通道</dt><dd>{developmentLane?.state === 'ACTIVE' ? '已切入 HoloLake' : '等待受控载体'}</dd></div><div><dt>当前开发线</dt><dd>{developmentLane?.laneId || '—'}</dd></div><div><dt>当前写入载体</dt><dd>{developmentLane?.ownerInstanceId || '—'}</dd></div><div><dt>当前结论</dt><dd>{developmentLane?.state === 'ACTIVE' ? '开发执行环境已由 HoloLake 持有单写通道' : status.codeRepositoryMountCount > 0 && status.pnccReceiptCount > 0 ? '已有可核验运行记录' : '接口已接入,尚无完整运行记录'}</dd></div></dl></section>
|
||||||
<section className="plain-panel">
|
<section className="plain-panel">
|
||||||
<header><div><h2>用户编号验证</h2><p>用户编号必须通过登记服务验证。验证结果不构成人格绑定、模型载体绑定或执行授权。</p></div><span className={zeroPoint?.route === 'verified' ? 'status-chip online' : 'status-chip'}>{zeroPoint ? (zeroPoint.route === 'verified' ? '验证有效' : '功能受限') : '正在读取'}</span></header>
|
<header><div><h2>用户编号验证</h2><p>用户编号必须通过登记服务验证。验证结果不构成人格绑定、模型载体绑定或执行授权。</p></div><span className={zeroPoint?.route === 'verified' ? 'status-chip online' : 'status-chip'}>{zeroPoint ? (zeroPoint.route === 'verified' ? '验证有效' : '功能受限') : '正在读取'}</span></header>
|
||||||
<dl className="evidence-list">
|
<dl className="evidence-list">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue