feat: embed cross-platform HoloLake terminal link

This commit is contained in:
冰朔 2026-08-17 18:59:37 +08:00
commit ae7f883891
16 changed files with 808 additions and 122 deletions

View file

@ -60,12 +60,14 @@ MCP 只保留为外部工具适配入口;它断开或编程 AI 重启时,活
写车道仍留在 HoloLake 中。写车道不会因连接断开而自动释放,必须由持有恢复钥匙的同一车道
显式释放并产生回执,下一条车道才能取得写权限。
当前已实现并注册账号级单写车道内核;外部本机代理与增量仓库对象通道仍处于后续迁入门,
不得把本轮内核实现误报为外部编程 AI 已经完成直连。
当前已实现账号级单写车道与外部编程 AI 终端直连协议。macOS、Linux 使用用户私有 Unix
SocketWindows 使用所有者与系统账户私有 Named Pipe三者共用会话、心跳、断线续接、
精确事件游标与工作环境事实帧。连接、续接、取得写车道及心跳都会让 HoloLake 返回当前
运行所有者、写入者匹配、GLS 原生协议内核、有效期与摘要,模型不再负责通读协议正文恢复环境。
尚未实现的是第二阶段的受监督 Shell、通用 Agent 工具循环、发布与部署执行器。
编程 AI 不允许把永久依赖 MCP 当成能力终态。若首次进入时缺少 HoloLake 本机直连器,它应在
当前唯一写车道内补齐、测试并登记该连接器;经声明权限和人类批准后安装到 HoloLake迁移为
可认证、可恢复的本机直连。MCP 随后只保留为发现、恢复与兼容入口。
编程 AI 不允许把永久依赖 MCP 当成能力终态。已安装 HoloLake 的 `--connector` 是可认证、
可恢复的本机直连入口MCP 只保留为发现、恢复与兼容入口。
## 当前收束与下一门

View file

@ -12,6 +12,7 @@
"transport_is_authority": false
},
"mcp_role": "OPTIONAL_EXTERNAL_TOOL_ADAPTER_NOT_CONTINUITY_OR_AUTHORITY_ROOT",
"terminal_link_contract": "contracts/programming-ai-terminal-link.json",
"nearby_ai_discovery_contract": "contracts/nearby-ai-discovery.json",
"circular_lake_membrane_contract": "contracts/circular-lake-membrane.json",
"external_ai_entry": {
@ -111,6 +112,10 @@
"connector_capability_bootstrap_contract_registered": true,
"connector_capability_bootstrap_runtime": true,
"installed_app_connector_entry_runtime": true,
"cross_platform_local_transport_runtime": true,
"authenticated_heartbeat_runtime": true,
"hololake_work_environment_frame_runtime": true,
"model_protocol_context_restore_required": false,
"external_local_broker_runtime": true,
"authenticated_broker_development_lane_runtime": true,
"development_lane_human_projection_runtime": true,

View file

@ -6,7 +6,12 @@
"same_device": {
"auto_discovery": true,
"descriptor": "STANDARD_APP_DATA_DESCRIPTOR",
"transport": "USER_ONLY_UNIX_SOCKET",
"transport": {
"macos": "USER_PRIVATE_UNIX_SOCKET",
"linux": "USER_PRIVATE_UNIX_SOCKET",
"windows": "USER_PRIVATE_NAMED_PIPE"
},
"terminal_link_protocol": "HOLOLAKE_TERMINAL_LINK/2",
"copy_large_invitation_required": false,
"network_required": false
},

View file

@ -0,0 +1,62 @@
{
"schema": "hololake.programming-ai-terminal-link-contract/v2",
"record_id": "HLP-PROGRAMMING-AI-TERMINAL-LINK-002",
"state": "NATIVE_CROSS_PLATFORM_CONTROL_PLANE_IMPLEMENTED",
"purpose": "Keep an external programming AI attached to a HoloLake-owned development control plane without making MCP or chat context the continuity owner.",
"protocol": "HOLOLAKE_TERMINAL_LINK/2",
"platform_transports": {
"macos": "USER_PRIVATE_UNIX_SOCKET",
"linux": "USER_PRIVATE_UNIX_SOCKET",
"windows": "USER_PRIVATE_NAMED_PIPE"
},
"continuity": {
"owner": "HOLOLAKE",
"session_survives_ai_restart": true,
"session_survives_hololake_restart": true,
"connector_reloads_descriptor_after_transport_loss": true,
"uncertain_mutation_is_never_blindly_replayed": true,
"heartbeat_interval_ms": 15000,
"environment_frame_ttl_ms": 45000
},
"environment_frame": {
"schema": "hololake.programming-ai-work-environment/v1",
"required_after_open": true,
"required_after_resume": true,
"required_before_mutation": true,
"refreshes_on_authenticated_heartbeat": true,
"contains": [
"HOLOLAKE_RUNTIME_OWNER",
"DIRECT_TERMINAL_TRANSPORT",
"SESSION_AND_EVENT_CURSOR",
"DEVELOPMENT_LANE_AND_WRITER_MATCH",
"GLS_NATIVE_PROTOCOL_RUNTIME",
"FRAME_EXPIRY_AND_SHA256"
],
"protocol_restoration_by_model_required": false
},
"write_boundary": {
"account_write_lanes": 1,
"session_lane_must_match": true,
"session_client_must_match_writer": true,
"visitor_may_write": false,
"transport_is_authority": false,
"environment_frame_is_reality_execution_authority": false
},
"phase_boundary": {
"current_phase": "EXTERNAL_PROGRAMMING_AI_DIRECT_CONTROL_PLANE",
"supervised_shell_execution": false,
"general_agent_tool_loop": false,
"persona_memory_startup": "NEXT_PHASE_AFTER_DIRECT_LINK_ACCEPTANCE",
"age_agent_execution": "NEXT_PHASE_AFTER_DIRECT_LINK_ACCEPTANCE"
},
"acceptance": {
"macos_local_runtime": "PASS_DEVELOPER_ID_SIGNED_APP_LIVE_CONNECTOR_AND_UI_READBACK",
"macos_public_notarization": "PENDING_NEW_BINARY_SUBMISSION",
"linux_unix_socket_adapter_compile": "PASS_X86_64_UNKNOWN_LINUX_MUSL",
"linux_full_desktop_compile": "NOT_OBSERVED",
"linux_installed_runtime": "NOT_YET_OBSERVED",
"windows_named_pipe_adapter_compile": "PASS_X86_64_PC_WINDOWS_MSVC",
"windows_full_desktop_compile": "BLOCKED_LOCAL_WINDOWS_SDK_AND_LINKER_UNAVAILABLE",
"windows_installed_runtime": "NOT_YET_OBSERVED"
}
}

View file

@ -42,9 +42,9 @@ The visible shell is HoloLake itself. Git is the durable history engine below it
## 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.
MCP may discover HoloLake, but it does not own continuity. The installed application starts a same-account local broker: a mode-0600 Unix socket on macOS and Linux, or an owner/System-only Named Pipe on Windows. 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. The connector reloads the application descriptor after transport loss and never blindly replays an operation whose response is uncertain.
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.
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. Opening, resuming, acquiring and every authenticated heartbeat return or require a bounded HoloLake work-environment frame. That frame states the HoloLake runtime owner, session cursor, writer match, native GLS runtime, expiry and digest; the external model does not restore protocol prose from chat context. 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.
The zero-core protocol layer now compiles the numbered GLS sources pinned to the current REPO-012 commit into a deterministic native registry. The registry inventories every unique numbered source with its path and SHA-256, but only protocols with an explicit typed adapter, event set and dependency-closed projection may execute. Raw protocol prose and arbitrary code carried by a protocol are never executed. The first native enforcement adapter binds GLS-0253 identity and numbering rules to the human-number route, with GLS-0250, GLS-0262 and GLS-0263 as executable dependencies. Unknown namespaces, persona numbers presented as human numbers, missing adapters and unprojected protocols fail closed. The system page reports compiled, executable and not-yet-executable protocol counts without presenting inventory as enforcement.

View file

@ -0,0 +1,39 @@
# ADR 0007: Cross-platform programming-AI terminal link
- Status: implemented in source; installed acceptance remains per platform
- Date: 2026-08-17
## Context
An external programming AI must remain attached to a HoloLake-owned work environment without making MCP stability, one chat window, or model protocol recall the continuity root. The product is not macOS-only: Windows and Linux must enforce the same session and write semantics even though their local IPC primitives differ.
## Decision
Embed `HOLOLAKE_TERMINAL_LINK/2` in the native HoloLake executable. macOS and Linux use a mode-0600 Unix socket inside a mode-0700 runtime directory. Windows uses a local Named Pipe with a protected DACL granting full access only to Local System and the creating object owner. All platforms additionally require HoloLake session authentication.
The installed executable exposes `--connector` as sequential newline-delimited JSON. It reloads the broker descriptor after transport loss. If an operation may have reached HoloLake but its response was lost, the connector reports an uncertain response and does not blindly replay it.
Opening or resuming a session establishes continuity in HoloLake. The authenticated client then acquires the account's single development writer and obtains a short-lived work-environment frame. Heartbeats refresh both durable session observation and that frame. The frame contains the HoloLake runtime owner, session and event cursor, writer match, compiled GLS runtime identity, expiry and digest. It explicitly says that model-side protocol restoration is not required.
## Boundary
This decision completes the first-stage control plane, not the second-stage Agent executor. It does not grant a supervised shell, arbitrary file mutation, build, publication, deployment, persona binding or reality-execution authority. Those require separate typed operations, approvals and receipts.
Cross-compilation of the isolated Linux Unix-socket and Windows Named-Pipe adapters is evidence for source portability only. It is not installed-runtime acceptance. A full Windows desktop build remains unobserved on this macOS builder because the Windows SDK and linker are unavailable; Linux and Windows installed readback must occur on their real targets.
## Rejected alternatives
- TCP loopback as the common denominator: it broadens the local attack surface and weakens the operating-system account boundary.
- A macOS implementation with Windows and Linux documentation only: transport portability without compiled adapters is not an implementation.
- Replaying a request automatically after a broken response: a mutation could execute twice.
- Asking every newly started model to reread all GLS prose: protocols are HoloLake runtime code, not model memory.
- Calling the control plane an Agent shell: the supervised execution organ is the next phase.
## 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/programming-ai-terminal-link.json`
- `scripts/programming-ai-terminal-link.test.mjs`

View file

@ -0,0 +1,44 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const contract = JSON.parse(readFileSync(new URL('../contracts/programming-ai-terminal-link.json', import.meta.url)))
const broker = readFileSync(new URL('../src-tauri/src/direct_local_broker.rs', import.meta.url), 'utf8')
const session = readFileSync(new URL('../src-tauri/src/direct_local_session.rs', import.meta.url), 'utf8')
const lib = readFileSync(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf8')
test('terminal link has native local transports for macOS Linux and Windows', () => {
assert.equal(contract.protocol, 'HOLOLAKE_TERMINAL_LINK/2')
assert.equal(contract.platform_transports.macos, 'USER_PRIVATE_UNIX_SOCKET')
assert.equal(contract.platform_transports.linux, 'USER_PRIVATE_UNIX_SOCKET')
assert.equal(contract.platform_transports.windows, 'USER_PRIVATE_NAMED_PIPE')
assert.match(broker, /GenericFilePath/)
assert.match(broker, /GenericNamespaced/)
assert.match(broker, /WINDOWS_NAMED_PIPE_JSON_LINES/)
assert.match(broker, /D:P\(A;;GA;;;SY\)\(A;;GA;;;OW\)/)
assert.doesNotMatch(lib, /direct_local_broker_windows/)
})
test('HoloLake owns continuity and supplies a bounded work-environment fact frame', () => {
assert.equal(contract.continuity.owner, 'HOLOLAKE')
assert.equal(contract.environment_frame.protocol_restoration_by_model_required, false)
assert.equal(contract.environment_frame.required_before_mutation, true)
assert.match(broker, /VERIFIED_HOLOLAKE_WORK_ENVIRONMENT/)
assert.match(broker, /GLS_RUNTIME_MANIFEST_V2_AND_NATIVE_KERNEL/)
assert.match(broker, /refresh_before_mutation_required/)
assert.match(broker, /protocol_restoration_by_model_required: false/)
})
test('heartbeats refresh the session and environment without granting a shell', () => {
assert.equal(contract.continuity.heartbeat_interval_ms, 15000)
assert.equal(contract.phase_boundary.supervised_shell_execution, false)
assert.match(session, /HEARTBEAT_ACK/)
assert.match(broker, /HEARTBEAT_ACK_ENVIRONMENT_REFRESHED/)
assert.match(lib, /heartbeat_direct_local_session/)
})
test('transport recovery never blindly replays an uncertain mutation', () => {
assert.equal(contract.continuity.uncertain_mutation_is_never_blindly_replayed, true)
assert.match(broker, /HOLOLAKE_TERMINAL_LINK_RESPONSE_UNCERTAIN/)
assert.match(broker, /safeToRetryAfterStatusRead/)
})

View file

@ -789,6 +789,12 @@ dependencies = [
"syn 2.0.115",
]
[[package]]
name = "doctest-file"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359"
[[package]]
name = "document-features"
version = "0.2.12"
@ -1492,6 +1498,7 @@ dependencies = [
"dirs",
"fs2",
"futures-util",
"interprocess",
"reqwest",
"ring",
"rusqlite",
@ -1507,6 +1514,7 @@ dependencies = [
"tokio",
"url",
"uuid",
"widestring",
]
[[package]]
@ -1804,6 +1812,19 @@ dependencies = [
"cfb",
]
[[package]]
name = "interprocess"
version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "798de1433ba514cc6c04c4144c2469af81396e4906195218737c776d47769572"
dependencies = [
"doctest-file",
"libc",
"recvmsg",
"widestring",
"windows-sys 0.61.2",
]
[[package]]
name = "ipnet"
version = "2.12.1"
@ -3022,7 +3043,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@ -3159,6 +3180,12 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.18"
@ -5154,6 +5181,12 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -17,6 +17,7 @@ tauri-build = { version = "2.5.4", features = [] }
[dependencies]
dirs = "6"
fs2 = "0.4"
interprocess = "2.4.2"
ring = "0.17"
rusqlite = { version = "0.31", features = ["bundled"] }
base64 = "0.22"
@ -33,5 +34,8 @@ reqwest = { version = "0.13.2", default-features = false, features = ["cookies",
tokio = { version = "1", features = ["time"] }
futures-util = "0.3"
[target.'cfg(windows)'.dependencies]
widestring = "1"
[dev-dependencies]
tempfile = "3"

View file

@ -1,9 +1,9 @@
use crate::circular_lake_membrane::{receive_at as receive_language_at, ReceiveLanguageInput};
use crate::direct_local_session::{
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,
heartbeat_at, 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,
@ -29,13 +29,23 @@ use crate::pncc_remote_git::{
use crate::pncc_repository_binding::{
inspect_mounted_at as inspect_mounted_pncc_at, InspectMountedPnccRepositoryInput,
};
#[cfg(unix)]
use interprocess::local_socket::GenericFilePath;
#[cfg(windows)]
use interprocess::local_socket::GenericNamespaced;
use interprocess::local_socket::{prelude::*, ListenerNonblockingMode, ListenerOptions};
#[cfg(windows)]
use interprocess::os::windows::{
local_socket::ListenerOptionsExt, security_descriptor::SecurityDescriptor,
};
use interprocess::TryClone;
use ring::digest::{digest, SHA256};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs::{self, OpenOptions};
use std::io::{self, BufRead, BufReader, Read, Write};
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
@ -45,10 +55,26 @@ use std::thread;
use std::time::Duration;
use tauri::{AppHandle, Manager};
use uuid::Uuid;
#[cfg(windows)]
use widestring::U16CString;
#[cfg(windows)]
trait OpenOptionsModeExt {
fn mode(&mut self, mode: u32) -> &mut Self;
}
#[cfg(windows)]
impl OpenOptionsModeExt for OpenOptions {
fn mode(&mut self, _mode: u32) -> &mut Self {
self
}
}
const BROKER_SCHEMA: &str = "hololake.direct-local-broker/v1";
const MAX_REQUEST_BYTES: u64 = 128 * 1024;
const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1";
const TERMINAL_LINK_PROTOCOL: &str = "HOLOLAKE_TERMINAL_LINK/2";
const ENVIRONMENT_FRAME_TTL_MS: u128 = 45_000;
pub struct DirectLocalBrokerHandle {
shutdown: Arc<AtomicBool>,
@ -131,7 +157,7 @@ struct BrokerStorageRoots {
impl Drop for DirectLocalBrokerHandle {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Release);
let _ = UnixStream::connect(&self.socket_path);
let _ = connect_endpoint(&self.socket_path);
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
@ -152,6 +178,8 @@ enum BrokerRequest {
Ping,
OpenSession(OpenSessionInput),
ResumeSession(ResumeSessionInput),
HeartbeatSession(AuthenticateSessionInput),
GetWorkEnvironment(AuthenticatedWorkEnvironmentInput),
AppendEvent(AppendSessionEventInput),
ResolveCapabilityRoute(AuthenticatedRouteInput),
InstallDynamicNodeRegistry(AuthenticatedRegistryInput),
@ -263,6 +291,49 @@ struct AuthenticatedDevelopmentReleaseInput {
lane: ReleaseWriteLaneInput,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AuthenticatedWorkEnvironmentInput {
session: AuthenticateSessionInput,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct WorkEnvironmentFrame {
schema: &'static str,
state: &'static str,
environment_id: String,
runtime_owner: &'static str,
continuity_owner: &'static str,
transport: &'static str,
mcp_role: &'static str,
protocol_runtime: &'static str,
account_key: String,
session_id: String,
lane_id: String,
client_instance_id: String,
session_event_cursor: u64,
session_observed_at_unix_ms: u128,
development_write_lane_state: String,
development_writer_matches_session: bool,
workspace_authority: &'static str,
supervised_shell_execution: bool,
protocol_restoration_by_model_required: bool,
refresh_before_mutation_required: bool,
issued_at_unix_ms: u128,
valid_until_unix_ms: u128,
frame_sha256: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct HeartbeatEnvironmentReceipt {
schema: &'static str,
state: &'static str,
session: DirectSessionReceipt,
environment: WorkEnvironmentFrame,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BrokerResponse {
@ -314,7 +385,11 @@ fn nearby_discovery_snapshot() -> NearbyAiDiscoverySnapshot {
schema: DISCOVERY_SCHEMA,
state: "DISCOVERABLE_ON_SAME_DEVICE",
service_name: "HoloLake",
transport: "STANDARD_APP_DATA_DESCRIPTOR_TO_USER_ONLY_UNIX_SOCKET",
transport: if cfg!(windows) {
"STANDARD_APP_DATA_DESCRIPTOR_TO_USER_PRIVATE_NAMED_PIPE"
} else {
"STANDARD_APP_DATA_DESCRIPTOR_TO_USER_PRIVATE_UNIX_SOCKET"
},
language_protocol: "GLP/1.0",
automatic_same_device_discovery: true,
large_invitation_copy_required: false,
@ -327,50 +402,138 @@ fn nearby_discovery_snapshot() -> NearbyAiDiscoverySnapshot {
pub fn run_connector() -> Result<(), String> {
let descriptor_path = connector_descriptor_path()?;
let stdin = io::stdin();
let mut stdout = io::stdout().lock();
let mut stream = None;
for line in stdin.lock().lines() {
let line = line.map_err(|error| format!("HOLOLAKE_CONNECTOR_INPUT_FAILED: {error}"))?;
if line.trim().is_empty() {
continue;
}
if line.len() as u64 > MAX_REQUEST_BYTES {
write_connector_transport_error(&mut stdout, "HOLOLAKE_BROKER_REQUEST_TOO_LARGE")?;
continue;
}
let needs_connection = match stream.as_mut() {
Some(connected) => probe_connection(connected).is_err(),
None => true,
};
if needs_connection {
let descriptor = read_connector_descriptor(&descriptor_path)?;
stream = Some(connect_with_startup_grace(&descriptor)?);
}
let result = send_connector_request(
stream
.as_mut()
.ok_or("HOLOLAKE_DIRECT_LOCAL_BROKER_NOT_RUNNING")?,
line.as_bytes(),
);
match result {
Ok(response) => {
stdout
.write_all(&response)
.and_then(|_| stdout.flush())
.map_err(|error| format!("HOLOLAKE_CONNECTOR_OUTPUT_FAILED: {error}"))?;
}
Err(error) => {
stream = None;
write_connector_transport_error(
&mut stdout,
&format!("HOLOLAKE_TERMINAL_LINK_RESPONSE_UNCERTAIN: {error}"),
)?;
}
}
}
Ok(())
}
fn read_connector_descriptor(path: &Path) -> Result<ConnectorDescriptor, String> {
let descriptor: ConnectorDescriptor = serde_json::from_slice(
&fs::read(&descriptor_path)
&fs::read(path)
.map_err(|error| format!("HOLOLAKE_BROKER_DESCRIPTOR_UNAVAILABLE: {error}"))?,
)
.map_err(|error| format!("HOLOLAKE_BROKER_DESCRIPTOR_INVALID: {error}"))?;
if descriptor.schema != BROKER_SCHEMA
|| descriptor.state != "LISTENING"
|| descriptor.transport != "UNIX_STREAM_JSON_LINES"
|| !matches!(
descriptor.transport.as_str(),
"UNIX_STREAM_JSON_LINES" | "WINDOWS_NAMED_PIPE_JSON_LINES"
)
{
return Err("HOLOLAKE_BROKER_DESCRIPTOR_UNSUPPORTED".into());
}
let mut stream = connect_with_startup_grace(&descriptor)?;
Ok(descriptor)
}
fn probe_connection(stream: &mut LocalSocketStream) -> Result<(), String> {
let response = send_connector_request(stream, br#"{"operation":"PING"}"#)?;
let value: Value = serde_json::from_slice(&response)
.map_err(|_| "HOLOLAKE_CONNECTOR_PROBE_INVALID".to_string())?;
if value.get("ok").and_then(Value::as_bool) == Some(true)
&& value
.pointer("/result/terminalLinkProtocol")
.and_then(Value::as_str)
== Some(TERMINAL_LINK_PROTOCOL)
{
Ok(())
} else {
Err("HOLOLAKE_CONNECTOR_PROBE_REJECTED".into())
}
}
fn send_connector_request(
stream: &mut LocalSocketStream,
request: &[u8],
) -> Result<Vec<u8>, String> {
stream
.write_all(request)
.and_then(|_| stream.write_all(b"\n"))
.and_then(|_| stream.flush())
.map_err(|error| format!("HOLOLAKE_CONNECTOR_WRITE_FAILED: {error}"))?;
let read_stream = stream
.try_clone()
.map_err(|error| format!("HOLOLAKE_CONNECTOR_CLONE_FAILED: {error}"))?;
let output = thread::Builder::new()
.name("hololake-connector-output".into())
.spawn(move || -> Result<(), String> {
let mut reader = BufReader::new(read_stream);
let mut stdout = io::stdout().lock();
io::copy(&mut reader, &mut stdout)
.and_then(|_| stdout.flush())
.map_err(|error| format!("HOLOLAKE_CONNECTOR_OUTPUT_FAILED: {error}"))?;
Ok(())
})
.map_err(|error| format!("HOLOLAKE_CONNECTOR_OUTPUT_THREAD_FAILED: {error}"))?;
let mut stdin = io::stdin().lock();
io::copy(&mut stdin, &mut stream)
.and_then(|_| stream.shutdown(std::net::Shutdown::Write))
.map_err(|error| format!("HOLOLAKE_CONNECTOR_INPUT_FAILED: {error}"))?;
output
.join()
.map_err(|_| "HOLOLAKE_CONNECTOR_OUTPUT_THREAD_PANICKED".to_string())??;
Ok(())
let mut response = Vec::new();
BufReader::new(read_stream)
.take(MAX_REQUEST_BYTES + 1)
.read_until(b'\n', &mut response)
.map_err(|error| format!("HOLOLAKE_CONNECTOR_READ_FAILED: {error}"))?;
if response.is_empty()
|| !response.ends_with(b"\n")
|| response.len() as u64 > MAX_REQUEST_BYTES
{
return Err("HOLOLAKE_CONNECTOR_RESPONSE_INVALID".into());
}
Ok(response)
}
fn connect_with_startup_grace(descriptor: &ConnectorDescriptor) -> Result<UnixStream, String> {
fn write_connector_transport_error(output: &mut impl Write, error: &str) -> Result<(), String> {
serde_json::to_writer(
&mut *output,
&serde_json::json!({
"schema": BROKER_SCHEMA,
"ok": false,
"error": error,
"continuityOwner": "HOLOLAKE",
"safeToRetryAfterStatusRead": true
}),
)
.map_err(|failure| format!("HOLOLAKE_CONNECTOR_OUTPUT_FAILED: {failure}"))?;
output
.write_all(b"\n")
.and_then(|_| output.flush())
.map_err(|failure| format!("HOLOLAKE_CONNECTOR_OUTPUT_FAILED: {failure}"))
}
fn connect_with_startup_grace(
descriptor: &ConnectorDescriptor,
) -> Result<LocalSocketStream, 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) {
match connect_endpoint(Path::new(&descriptor.socket_path)) {
Ok(stream) => return Ok(stream),
Err(error)
if matches!(
@ -392,6 +555,59 @@ fn connect_with_startup_grace(descriptor: &ConnectorDescriptor) -> Result<UnixSt
))
}
fn connect_endpoint(path: &Path) -> std::io::Result<LocalSocketStream> {
#[cfg(unix)]
{
let name = path.to_fs_name::<GenericFilePath>()?;
return LocalSocketStream::connect(name);
}
#[cfg(windows)]
{
let endpoint = path.to_string_lossy();
let name = endpoint.as_ref().to_ns_name::<GenericNamespaced>()?;
LocalSocketStream::connect(name)
}
}
fn listen_endpoint(path: &Path) -> Result<LocalSocketListener, String> {
#[cfg(unix)]
let name = path
.to_fs_name::<GenericFilePath>()
.map_err(|error| format!("HOLOLAKE_BROKER_NAME_INVALID: {error}"))?;
#[cfg(windows)]
let endpoint = path.to_string_lossy();
#[cfg(windows)]
let name = endpoint
.as_ref()
.to_ns_name::<GenericNamespaced>()
.map_err(|error| format!("HOLOLAKE_BROKER_NAME_INVALID: {error}"))?;
let options = ListenerOptions::new().name(name);
#[cfg(windows)]
let options = options.security_descriptor(user_private_windows_security_descriptor()?);
options
.create_sync()
.map_err(|error| format!("HOLOLAKE_BROKER_BIND_FAILED: {error}"))
}
/// The protected DACL grants full access only to Local System and the object
/// owner. The named-pipe owner is the HoloLake desktop account that creates it.
/// HoloLake session authentication remains mandatory above this OS boundary.
#[cfg(windows)]
fn user_private_windows_security_descriptor() -> Result<SecurityDescriptor, String> {
let sddl = U16CString::from_str("D:P(A;;GA;;;SY)(A;;GA;;;OW)")
.map_err(|error| format!("HOLOLAKE_BROKER_WINDOWS_DACL_INVALID: {error}"))?;
SecurityDescriptor::deserialize(&sddl)
.map_err(|error| format!("HOLOLAKE_BROKER_WINDOWS_DACL_FAILED: {error}"))
}
fn platform_transport() -> &'static str {
if cfg!(windows) {
"WINDOWS_NAMED_PIPE_JSON_LINES"
} else {
"UNIX_STREAM_JSON_LINES"
}
}
fn connector_descriptor_path() -> Result<PathBuf, String> {
if let Some(path) = std::env::var_os("HOLOLAKE_BROKER_DESCRIPTOR") {
return Ok(PathBuf::from(path));
@ -442,26 +658,28 @@ fn start_at(
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}"))?;
#[cfg(unix)]
if let Some(parent) = socket_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_DIR_FAILED: {error}"))?;
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_BROKER_RUNTIME_PERMISSIONS_FAILED: {error}"))?;
}
#[cfg(unix)]
if socket_path.exists() {
if UnixStream::connect(&socket_path).is_ok() {
if connect_endpoint(&socket_path).is_ok() {
return Err("HOLOLAKE_DIRECT_LOCAL_BROKER_ALREADY_RUNNING".into());
}
fs::remove_file(&socket_path)
.map_err(|error| format!("HOLOLAKE_BROKER_STALE_SOCKET_REMOVE_FAILED: {error}"))?;
}
let listener = UnixListener::bind(&socket_path)
.map_err(|error| format!("HOLOLAKE_BROKER_BIND_FAILED: {error}"))?;
let listener = listen_endpoint(&socket_path)?;
#[cfg(unix)]
fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))
.map_err(|error| format!("HOLOLAKE_BROKER_SOCKET_PERMISSIONS_FAILED: {error}"))?;
listener
.set_nonblocking(true)
.set_nonblocking(ListenerNonblockingMode::Accept)
.map_err(|error| format!("HOLOLAKE_BROKER_NONBLOCKING_FAILED: {error}"))?;
write_descriptor(
@ -469,7 +687,7 @@ fn start_at(
&BrokerDescriptor {
schema: BROKER_SCHEMA,
state: "LISTENING",
transport: "UNIX_STREAM_JSON_LINES",
transport: platform_transport(),
socket_path: socket_path.to_string_lossy().into_owned(),
max_request_bytes: MAX_REQUEST_BYTES,
process_id: std::process::id(),
@ -502,6 +720,7 @@ fn start_at(
&worker_shutdown,
&worker_authenticated_connections,
);
#[cfg(unix)]
let _ = fs::remove_file(worker_socket);
})
.map_err(|error| format!("HOLOLAKE_BROKER_THREAD_FAILED: {error}"))?;
@ -518,14 +737,14 @@ fn start_at(
}
fn serve(
listener: UnixListener,
listener: LocalSocketListener,
roots: BrokerStorageRoots,
shutdown: &AtomicBool,
authenticated_connections: &Arc<AtomicUsize>,
) {
while !shutdown.load(Ordering::Acquire) {
match listener.accept() {
Ok((stream, _)) => {
Ok(stream) => {
if shutdown.load(Ordering::Acquire) {
break;
}
@ -549,7 +768,7 @@ fn serve(
}
fn serve_connection(
mut stream: UnixStream,
mut stream: LocalSocketStream,
roots: &BrokerStorageRoots,
authenticated_connections: Arc<AtomicUsize>,
) {
@ -642,6 +861,8 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
BrokerRequest::Ping => serde_json::to_value(serde_json::json!({
"state": "READY",
"continuityOwner": "HOLOLAKE",
"terminalLinkProtocol": TERMINAL_LINK_PROTOCOL,
"transport": platform_transport(),
"mcpRole": "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY"
}))
.map_err(|error| error.to_string()),
@ -649,6 +870,25 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::ResumeSession(input) => resume_at(session_root, input)
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::HeartbeatSession(input) => heartbeat_at(session_root, &input)
.and_then(|session| {
work_environment_frame_at(
session_root,
development_root,
&AuthenticatedWorkEnvironmentInput { session: input },
)
.map(|environment| HeartbeatEnvironmentReceipt {
schema: "hololake.terminal-link-heartbeat/v2",
state: "HEARTBEAT_ACK_ENVIRONMENT_REFRESHED",
session,
environment,
})
})
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::GetWorkEnvironment(input) => {
work_environment_frame_at(session_root, development_root, &input)
.and_then(|frame| serde_json::to_value(frame).map_err(|error| error.to_string()))
}
BrokerRequest::AppendEvent(input) => append_event_at(session_root, input)
.and_then(|receipt| serde_json::to_value(receipt).map_err(|error| error.to_string())),
BrokerRequest::ResolveCapabilityRoute(input) => {
@ -719,7 +959,9 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
.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)
let session = input.session;
let account_id = input.lane.account_id.clone();
authenticate_development_request(session_root, &session, &account_id)
.and_then(|context| {
if context.lane_id != input.lane.lane_id
|| context.client_instance_id != input.lane.owner_instance_id
@ -729,7 +971,21 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
acquire_development_lane_at(development_root, input.lane)
})
.and_then(|receipt| {
serde_json::to_value(receipt).map_err(|error| error.to_string())
let mut value =
serde_json::to_value(receipt).map_err(|error| error.to_string())?;
let environment = work_environment_frame_at(
session_root,
development_root,
&AuthenticatedWorkEnvironmentInput { session },
)?;
value
.as_object_mut()
.ok_or("HOLOLAKE_DEVELOPMENT_LANE_RECEIPT_INVALID")?
.insert(
"environment".into(),
serde_json::to_value(environment).map_err(|error| error.to_string())?,
);
Ok(value)
})
}
BrokerRequest::InspectDevelopmentWriteLane(input) => {
@ -760,6 +1016,85 @@ fn dispatch(roots: &BrokerStorageRoots, bytes: &[u8]) -> BrokerResponse {
}
}
fn work_environment_frame_at(
session_root: &Path,
development_root: &Path,
input: &AuthenticatedWorkEnvironmentInput,
) -> Result<WorkEnvironmentFrame, String> {
let context =
authenticate_development_request(session_root, &input.session, &input.session.account_id)?;
let writer = inspect_development_lane_at(development_root, &input.session.account_id)?;
let writer_matches = writer.state == "ACTIVE"
&& writer.lane_id.as_deref() == Some(context.lane_id.as_str())
&& writer.owner_instance_id.as_deref() == Some(context.client_instance_id.as_str());
let state = if writer_matches {
"VERIFIED_HOLOLAKE_WORK_ENVIRONMENT"
} else {
"HOLOLAKE_SESSION_CONNECTED_WRITE_LANE_REQUIRED"
};
let issued_at_unix_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis())
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))?;
let valid_until_unix_ms = issued_at_unix_ms + ENVIRONMENT_FRAME_TTL_MS;
let environment_id = format!(
"env-{}",
&sha256_hex(
format!(
"{}\n{}\n{}\n{}",
context.account_key,
context.session_id,
context.lane_id,
context.client_instance_id
)
.as_bytes()
)[..24]
);
let frame_sha256 = sha256_hex(
format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
environment_id,
state,
context.session_id,
context.lane_id,
context.client_instance_id,
context.last_event_sequence,
writer.state,
valid_until_unix_ms
)
.as_bytes(),
);
Ok(WorkEnvironmentFrame {
schema: "hololake.programming-ai-work-environment/v1",
state,
environment_id,
runtime_owner: "HOLOLAKE_NATIVE_DESKTOP",
continuity_owner: "HOLOLAKE",
transport: TERMINAL_LINK_PROTOCOL,
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
protocol_runtime: "GLS_RUNTIME_MANIFEST_V2_AND_NATIVE_KERNEL",
account_key: context.account_key,
session_id: context.session_id,
lane_id: context.lane_id,
client_instance_id: context.client_instance_id,
session_event_cursor: context.last_event_sequence,
session_observed_at_unix_ms: context.observed_at_unix_ms,
development_write_lane_state: writer.state.to_string(),
development_writer_matches_session: writer_matches,
workspace_authority: if writer_matches {
"HOLOLAKE_CONTROL_PLANE_ACTIVE_REGISTERED_CODE_CHANNEL_REQUIRED"
} else {
"NO_MUTATION_UNTIL_HOLOLAKE_WRITE_LANE"
},
supervised_shell_execution: false,
protocol_restoration_by_model_required: false,
refresh_before_mutation_required: true,
issued_at_unix_ms,
valid_until_unix_ms,
frame_sha256,
})
}
fn authenticate_development_request(
session_root: &Path,
session: &AuthenticateSessionInput,
@ -860,9 +1195,13 @@ fn short_socket_path(app_data: &Path) -> PathBuf {
.take(8)
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
std::env::temp_dir()
.join(format!("hololake-{key}"))
.join("broker.sock")
if cfg!(windows) {
PathBuf::from(format!("LOCAL\\world.guanghu.hololake.{key}"))
} else {
std::env::temp_dir()
.join(format!("hololake-{key}"))
.join("broker.sock")
}
}
fn write_descriptor(path: &Path, descriptor: &BrokerDescriptor) -> Result<(), String> {
@ -894,7 +1233,7 @@ mod tests {
use tempfile::TempDir;
fn request(socket: &Path, value: Value) -> Value {
let mut stream = UnixStream::connect(socket).unwrap();
let mut stream = connect_endpoint(socket).unwrap();
serde_json::to_writer(&mut stream, &value).unwrap();
stream.write_all(b"\n").unwrap();
let mut line = String::new();
@ -1305,6 +1644,14 @@ mod tests {
);
assert_eq!(acquired["ok"], true);
assert_eq!(acquired["result"]["state"], "ACQUIRED");
assert_eq!(
acquired["result"]["environment"]["state"],
"VERIFIED_HOLOLAKE_WORK_ENVIRONMENT"
);
assert_eq!(
acquired["result"]["environment"]["protocolRestorationByModelRequired"],
false
);
let inspected = request(
&socket,
@ -1355,6 +1702,99 @@ mod tests {
assert_eq!(available["result"]["state"], "AVAILABLE");
}
#[test]
fn heartbeat_refreshes_a_verified_hololake_work_environment_frame() {
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: "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 session = serde_json::json!({
"accountId": "human-1",
"sessionId": opened["result"]["sessionId"],
"resumeSecret": opened["result"]["resumeSecret"]
});
let before_lane = request(
&socket,
serde_json::json!({
"operation": "GET_WORK_ENVIRONMENT",
"input": { "session": session }
}),
);
assert_eq!(
before_lane["result"]["state"],
"HOLOLAKE_SESSION_CONNECTED_WRITE_LANE_REQUIRED"
);
assert_eq!(
before_lane["result"]["protocolRestorationByModelRequired"],
false
);
request(
&socket,
serde_json::json!({
"operation": "ACQUIRE_DEVELOPMENT_WRITE_LANE",
"input": {
"session": session,
"lane": {
"accountId": "human-1",
"laneId": "DEV-1",
"ownerInstanceId": "codex-1",
"resumeToken": null
}
}
}),
);
let heartbeat = request(
&socket,
serde_json::json!({
"operation": "HEARTBEAT_SESSION",
"input": session
}),
);
assert_eq!(
heartbeat["result"]["state"],
"HEARTBEAT_ACK_ENVIRONMENT_REFRESHED"
);
assert_eq!(
heartbeat["result"]["environment"]["state"],
"VERIFIED_HOLOLAKE_WORK_ENVIRONMENT"
);
assert_eq!(
heartbeat["result"]["environment"]["runtimeOwner"],
"HOLOLAKE_NATIVE_DESKTOP"
);
assert_eq!(
heartbeat["result"]["environment"]["developmentWriterMatchesSession"],
true
);
}
#[test]
fn expression_only_visitor_cannot_acquire_a_development_lane() {
let temp = TempDir::new().unwrap();
@ -1411,7 +1851,7 @@ mod tests {
fs::create_dir_all(&routes).unwrap();
let broker = start_at(sessions.clone(), routes, descriptor, socket.clone()).unwrap();
let probe = UnixStream::connect(&socket).unwrap();
let probe = connect_endpoint(&socket).unwrap();
thread::sleep(Duration::from_millis(50));
assert_eq!(broker.active_connection_count(), 0);
drop(probe);
@ -1425,7 +1865,7 @@ mod tests {
},
)
.unwrap();
let mut connector = UnixStream::connect(&socket).unwrap();
let mut connector = connect_endpoint(&socket).unwrap();
serde_json::to_writer(
&mut connector,
&serde_json::json!({

View file

@ -1,57 +0,0 @@
use serde::Serialize;
use tauri::AppHandle;
const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1";
/// Windows currently has no Unix-domain socket transport. Keep the public app
/// launchable while reporting the connector boundary as closed, rather than
/// silently opening a TCP listener with weaker local-user isolation.
#[derive(Default)]
pub struct DirectLocalBrokerState;
impl DirectLocalBrokerState {
pub fn ensure_started(&self, _app: &AppHandle) -> Result<bool, String> {
Ok(false)
}
pub fn active_connection_count(&self) -> usize {
0
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NearbyAiDiscoverySnapshot {
pub schema: &'static str,
pub state: &'static str,
pub service_name: &'static str,
pub transport: &'static str,
pub language_protocol: &'static str,
pub automatic_same_device_discovery: bool,
pub large_invitation_copy_required: bool,
pub generic_ai_visitor: &'static str,
pub guanghu_persona: &'static str,
pub local_network_discovery: &'static str,
pub authority: &'static str,
}
#[tauri::command]
pub fn get_nearby_ai_discovery() -> NearbyAiDiscoverySnapshot {
NearbyAiDiscoverySnapshot {
schema: DISCOVERY_SCHEMA,
state: "WINDOWS_TRANSPORT_NOT_YET_IMPLEMENTED",
service_name: "HoloLake",
transport: "CLOSED_NO_TCP_FALLBACK",
language_protocol: "GLP/1.0",
automatic_same_device_discovery: false,
large_invitation_copy_required: false,
generic_ai_visitor: "DEFERRED",
guanghu_persona: "BINDING_EVIDENCE_REQUIRED",
local_network_discovery: "DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL",
authority: "UNAVAILABLE_IS_NOT_AUTHORIZATION",
}
}
pub fn run_connector() -> Result<(), String> {
Err("HOLOLAKE_WINDOWS_DIRECT_LOCAL_CONNECTOR_NOT_IMPLEMENTED".into())
}

View file

@ -38,6 +38,12 @@ pub struct DiscoveryTicketReceipt {
pub client_instance_id: String,
pub discovery_ticket: String,
pub issued_at_unix_ms: u128,
pub terminal_transport: &'static str,
pub connector_executable: String,
pub connector_arguments: Vec<&'static str>,
pub heartbeat_interval_ms: u64,
pub environment_refresh_interval_ms: u64,
pub required_bootstrap_operations: Vec<&'static str>,
pub receipt_id: String,
}
@ -76,6 +82,8 @@ pub(crate) struct AuthenticatedSessionContext {
pub session_id: String,
pub lane_id: String,
pub client_instance_id: String,
pub observed_at_unix_ms: u128,
pub last_event_sequence: u64,
}
#[derive(Clone, Debug, Serialize)]
@ -91,6 +99,10 @@ pub struct DirectSessionReceipt {
pub observed_at_unix_ms: u128,
pub last_event_sequence: u64,
pub resume_secret: Option<String>,
pub continuity_owner: &'static str,
pub transport: &'static str,
pub mcp_role: &'static str,
pub required_next_operation: &'static str,
pub receipt_id: String,
}
@ -199,6 +211,17 @@ pub async fn append_direct_local_session_event(
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn heartbeat_direct_local_session(
app: AppHandle,
input: AuthenticateSessionInput,
) -> Result<DirectSessionReceipt, String> {
let root = direct_session_root(&app)?;
tauri::async_runtime::spawn_blocking(move || heartbeat_at(&root, &input))
.await
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_JOIN_FAILED: {error}"))?
}
pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = crate::authenticated_storage::account_storage_root(app, "direct-local-session-v1")?;
fs::create_dir_all(&root)
@ -356,6 +379,18 @@ pub(crate) fn issue_ticket_at(
client_instance_id: input.client_instance_id,
discovery_ticket,
issued_at_unix_ms,
terminal_transport: "HOLOLAKE_TERMINAL_LINK/2",
connector_executable: std::env::current_exe()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|_| "HoloLake".into()),
connector_arguments: vec!["--connector"],
heartbeat_interval_ms: 15_000,
environment_refresh_interval_ms: 30_000,
required_bootstrap_operations: vec![
"OPEN_SESSION",
"ACQUIRE_DEVELOPMENT_WRITE_LANE",
"GET_WORK_ENVIRONMENT",
],
receipt_id: sha256_hex(
format!("ISSUED\n{discovery_ticket_sha256}\n{issued_at_unix_ms}").as_bytes(),
),
@ -446,6 +481,29 @@ pub(crate) fn append_event_at(
Ok(event_receipt("APPENDED", &event))
}
pub(crate) fn heartbeat_at(
root: &Path,
input: &AuthenticateSessionInput,
) -> Result<DirectSessionReceipt, 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 mut record = read_session(&path)?;
authorize(
&record,
&account_key,
&input.session_id,
&input.resume_secret,
)?;
record.observed_at_unix_ms = now_unix_ms()?;
write_record_atomic(&path, &record)?;
Ok(session_receipt("HEARTBEAT_ACK", &record, None))
}
pub(crate) fn authenticate_with_lane_at(
root: &Path,
input: &AuthenticateSessionInput,
@ -493,6 +551,8 @@ pub(crate) fn authenticate_context_at(
session_id: record.session_id,
lane_id: record.lane_id,
client_instance_id: record.client_instance_id,
observed_at_unix_ms: record.observed_at_unix_ms,
last_event_sequence: record.last_event_sequence,
})
}
@ -652,6 +712,10 @@ fn session_receipt(
observed_at_unix_ms: record.observed_at_unix_ms,
last_event_sequence: record.last_event_sequence,
resume_secret,
continuity_owner: "HOLOLAKE",
transport: "HOLOLAKE_TERMINAL_LINK/2",
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
required_next_operation: "GET_WORK_ENVIRONMENT",
receipt_id: sha256_hex(
format!(
"{state}\n{}\n{}\n{}\n{}",

View file

@ -21,6 +21,9 @@ pub struct HoloLakeHomeStatus {
pub release_recovery_state: String,
pub automatic_upstream_updates: bool,
pub mcp_role: &'static str,
pub terminal_link_protocol: &'static str,
pub terminal_link_transport: &'static str,
pub environment_frame_policy: &'static str,
}
#[tauri::command]
@ -41,6 +44,13 @@ pub fn get_hololake_home_status(
release_recovery_state: crate::release_update::release_recovery_state(&app)?,
automatic_upstream_updates: false,
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2",
terminal_link_transport: if cfg!(windows) {
"WINDOWS_USER_PRIVATE_NAMED_PIPE"
} else {
"MACOS_LINUX_USER_PRIVATE_UNIX_SOCKET"
},
environment_frame_policy: "REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION",
});
}
let session_root = direct_session_root(&app)?;
@ -57,5 +67,12 @@ pub fn get_hololake_home_status(
release_recovery_state: crate::release_update::release_recovery_state(&app)?,
automatic_upstream_updates: false,
mcp_role: "DISCOVERY_RECOVERY_COMPATIBILITY_ONLY",
terminal_link_protocol: "HOLOLAKE_TERMINAL_LINK/2",
terminal_link_transport: if cfg!(windows) {
"WINDOWS_USER_PRIVATE_NAMED_PIPE"
} else {
"MACOS_LINUX_USER_PRIVATE_UNIX_SOCKET"
},
environment_frame_policy: "REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION",
})
}

View file

@ -2,10 +2,6 @@ mod authenticated_storage;
mod circular_lake_membrane;
mod code_channel;
mod code_repo_login;
#[cfg(unix)]
mod direct_local_broker;
#[cfg(windows)]
#[path = "direct_local_broker_windows.rs"]
mod direct_local_broker;
mod direct_local_session;
mod dynamic_capability_routing;
@ -50,6 +46,7 @@ pub fn run() {
direct_local_session::open_direct_local_session,
direct_local_session::resume_direct_local_session,
direct_local_session::append_direct_local_session_event,
direct_local_session::heartbeat_direct_local_session,
direct_local_broker::get_nearby_ai_discovery,
gls_protocol_runtime::get_gls_protocol_runtime,
gls_protocol_kernel::get_gls_protocol_kernel,

View file

@ -854,6 +854,8 @@ mod tests {
session_id: "channel-session-1".into(),
lane_id: "codex-current-channel".into(),
client_instance_id: instance.into(),
observed_at_unix_ms: 1,
last_event_sequence: 0,
},
}
}

View file

@ -61,6 +61,9 @@ interface HomeStatus {
updateState: string
releaseRecoveryState: string
mcpRole: string
terminalLinkProtocol: string
terminalLinkTransport: string
environmentFramePolicy: string
}
interface DevelopmentWriteLaneStatus {
state: 'AVAILABLE' | 'ACTIVE'
@ -207,7 +210,17 @@ interface CodeTreeSnapshot { channelId: string; path: string; entries: CodeTreeE
interface CodeFileProjection { channelId: string; path: string; format: string; source: string; humanMarkdown: string; sizeBytes: number }
interface ReceiptEvent { sequence: number; kind: string; observedAtUnixMs: number; eventHash: string }
interface ReceiptProjection { events: ReceiptEvent[] }
interface DiscoveryTicketReceipt { laneId: string; clientInstanceId: string; discoveryTicket: string }
interface DiscoveryTicketReceipt {
laneId: string
clientInstanceId: string
discoveryTicket: string
terminalTransport: string
connectorExecutable: string
connectorArguments: string[]
heartbeatIntervalMs: number
environmentRefreshIntervalMs: number
requiredBootstrapOperations: string[]
}
interface NearbyAiDiscoverySnapshot {
state: string
serviceName: string
@ -331,7 +344,7 @@ interface EnterpriseReceiptEnvelope {
repository_projection?: EnterpriseReceiptProjection
}
const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY' }
const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'READY_HUMAN_CONFIRMATION_REQUIRED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY', terminalLinkProtocol: 'HOLOLAKE_TERMINAL_LINK/2', terminalLinkTransport: 'UNVERIFIED', environmentFramePolicy: 'REQUIRED_AFTER_CONNECT_RESUME_AND_BEFORE_MUTATION' }
const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], modules: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } }
const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false }
const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' }
@ -1189,8 +1202,18 @@ function HoloLakeApp() {
finally { setSystemBusy(false) }
}
const invitationText = ticket ? JSON.stringify({
schema: 'hololake.direct-local-invitation/v1',
connectorArgument: '--connector',
schema: 'hololake.direct-local-invitation/v2',
terminalTransport: ticket.terminalTransport,
connector: {
executable: ticket.connectorExecutable,
arguments: ticket.connectorArguments,
framing: 'JSON_LINES',
},
continuityOwner: 'HOLOLAKE',
mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY',
heartbeatIntervalMs: ticket.heartbeatIntervalMs,
environmentRefreshIntervalMs: ticket.environmentRefreshIntervalMs,
requiredBootstrapOperations: ticket.requiredBootstrapOperations,
openSession: {
operation: 'OPEN_SESSION',
input: {
@ -1200,6 +1223,12 @@ function HoloLakeApp() {
discoveryTicket: ticket.discoveryTicket,
},
},
afterOpen: [
'ACQUIRE_DEVELOPMENT_WRITE_LANE',
'GET_WORK_ENVIRONMENT',
],
beforeEveryMutation: 'GET_WORK_ENVIRONMENT',
protocolRestorationByModelRequired: false,
}, null, 2) : ''
const copyInvitation = async () => {
try {
@ -1381,7 +1410,7 @@ function HoloLakeApp() {
<div className="system-grid">
<section className="plain-panel connection-panel">
<header><div><h2></h2><p> AI HoloLakeMCP </p></div><span className={nearbyDiscovery?.automaticSameDeviceDiscovery ? 'status-chip online' : 'status-chip'}>{nearbyDiscovery?.automaticSameDeviceDiscovery ? '本机自动发现已开启' : '自动发现不可用'}</span></header>
<dl className="evidence-list"><div><dt></dt><dd>GLP/1.0 · </dd></div><div><dt> AI 访</dt><dd>{nearbyDiscovery?.genericAiVisitor === 'EXPRESSION_ONLY_READY' ? '可连接 · 仅语言表达' : '不可用'}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.guanghuPersona === 'BINDING_EVIDENCE_REQUIRED' ? '等待人格绑定证据' : '可连接'}</dd></div><div><dt></dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.localNetworkDiscovery === 'DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL' ? '等待加密传输与确认闭环' : '已开启'}</dd></div><div><dt>MCP</dt><dd> / </dd></div></dl>
<dl className="evidence-list"><div><dt></dt><dd>GLP/1.0 · </dd></div><div><dt> AI </dt><dd>{status.terminalLinkProtocol} · {status.directLocalBrokerState === 'READY' ? '原生通道就绪' : '等待账号登录'}</dd></div><div><dt></dt><dd>{status.terminalLinkTransport.includes('NAMED_PIPE') ? 'Windows 用户私有 Named Pipe' : 'macOS / Linux 用户私有 Unix Socket'}</dd></div><div><dt></dt><dd></dd></div><div><dt> AI 访</dt><dd>{nearbyDiscovery?.genericAiVisitor === 'EXPRESSION_ONLY_READY' ? '可连接 · 仅语言表达' : '不可用'}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.guanghuPersona === 'BINDING_EVIDENCE_REQUIRED' ? '等待人格绑定证据' : '可连接'}</dd></div><div><dt></dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{status.resumableSessionCount}</dd></div><div><dt></dt><dd>{nearbyDiscovery?.localNetworkDiscovery === 'DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL' ? '等待加密传输与确认闭环' : '已开启'}</dd></div><div><dt>MCP</dt><dd> / / </dd></div></dl>
<p className="boundary-note">访</p>
{ticket ? <><button className="secondary-button" type="button" onClick={() => void copyInvitation()}><Icon name="copy"/></button><pre className="invitation-data">{invitationText}</pre></> : <button className="secondary-button" type="button" disabled={systemBusy} onClick={() => void issueInvitation()}></button>}
</section>
@ -1399,7 +1428,7 @@ function HoloLakeApp() {
</dl> : <p className="boundary-note">线</p>}
<button className="secondary-button" type="button" disabled={serverPnccBusy} onClick={() => void refreshServerPncc()}>{serverPnccBusy ? '正在读取…' : '重新读取主控状态'}</button>
</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"><header><div><h2>HoloLake </h2><p>HoloLake AI </p></div><span className={developmentLane?.state === 'ACTIVE' ? 'status-chip online' : 'status-chip'}>{developmentLane?.state === 'ACTIVE' ? '环境已锚定' : '等待直连写入者'}</span></header><dl className="evidence-list"><div><dt></dt><dd>{status.terminalLinkProtocol}</dd></div><div><dt></dt><dd>HoloLake · </dd></div><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' ? '每次写入前必须持有未过期事实帧' : '未取得单写通道,不允许变更'}</dd></div><div><dt>Agent Shell</dt><dd> · </dd></div></dl></section>
<section className="plain-panel"><header><div><h2>GLS </h2><p></p></div><span className={glsRuntime?.state === 'ACTIVE_EXPLICIT_PROJECTIONS_ONLY' && glsRuntime.authorityConflictCount === 0 && glsRuntime.discoveredUnreconciledCount === 0 ? 'status-chip online' : 'status-chip'}>{glsRuntime ? '运行清单 v2 已加载' : '失败关闭'}</span></header>{glsRuntime ? <dl className="evidence-list"><div><dt></dt><dd>{glsRuntime.sourceRepository} · {glsRuntime.sourceCommit.slice(0, 12)}</dd></div><div><dt></dt><dd>{glsRuntime.protocolCount}</dd></div><div><dt></dt><dd>{glsRuntime.protocolRegistryIdCount}</dd></div><div><dt></dt><dd>{glsRuntime.registeredDraftCount} · {glsRuntime.registeredDraftNotStartedCount} </dd></div><div><dt></dt><dd>{glsRuntime.executableProjectionCount} · P0P6 {glsRuntime.implementationStageCount} </dd></div><div><dt></dt><dd>{glsRuntime.inventoriedNotExecutableCount}</dd></div><div><dt></dt><dd>{glsRuntime.typedSourceDependencyCount} · {glsRuntime.unclassifiedSourceDependencyCount}</dd></div><div><dt></dt><dd>{glsRuntime.legacyDependencyCycleCount} · </dd></div><div><dt></dt><dd>{glsRuntime.dependencyGapCount}</dd></div><div><dt> / </dt><dd>{glsRuntime.authorityConflictCount} / {glsRuntime.discoveredUnreconciledCount}</dd></div><div><dt></dt><dd>{glsRuntime.rawProtocolTextExecuted ? '允许' : '禁止'}</dd></div><div><dt></dt><dd>{glsRuntime.arbitraryProtocolCodeAllowed ? '允许' : '禁止'}</dd></div></dl> : <p className="boundary-note">GLS </p>}</section>
<section className="plain-panel"><header><div><h2>HoloLake </h2><p></p></div><span className={glsKernel?.state === 'P1_TO_P6_NATIVE_P7_FAIL_CLOSED' ? 'status-chip online' : 'status-chip'}>{glsKernel ? '随软件运行' : '失败关闭'}</span></header>{glsKernel ? <dl className="evidence-list"><div><dt>P1P6 </dt><dd>{glsKernel.executableProtocolCount} · {glsKernel.implementedStageCount} </dd></div><div><dt></dt><dd>{glsKernel.decisionReceiptCount}</dd></div><div><dt> / </dt><dd>{glsKernel.allowCount} / {glsKernel.denyCount}</dd></div><div><dt> / </dt><dd>{glsKernel.ambiguousCount} / {glsKernel.unverifiedCount}</dd></div><div><dt>GLC </dt><dd>{glsKernel.bootstrapCompilerSelfCheck === 'PASS_DETERMINISTIC_DOUBLE_COMPILE' ? '双编译一致' : '失败关闭'}</dd></div><div><dt>P7 </dt><dd>{glsKernel.p7VerifiedPhysicalCapabilityCount} · {glsKernel.p7NodeAssemblies.length} </dd></div><div><dt></dt><dd>{glsKernel.modelCanOverrideDecision ? '允许' : '禁止'}</dd></div><div><dt></dt><dd>{glsKernel.lastReceiptSha256 === 'GENESIS' ? '尚无裁决' : glsKernel.lastReceiptSha256.slice(0, 16)}</dd></div></dl> : <p className="boundary-note"></p>}</section>
<section className="plain-panel"><header><div><h2></h2><p></p></div><span className={numberingKernel?.state === 'ACTIVE_PINNED_AUTHORITY_MAP' ? 'status-chip online' : 'status-chip'}>{numberingKernel ? '本机内核已加载' : '失败关闭'}</span></header>{numberingKernel ? <dl className="evidence-list"><div><dt></dt><dd>{numberingKernel.authorityMapId}</dd></div><div><dt></dt><dd>{numberingKernel.authorityMapVersion}</dd></div><div><dt></dt><dd>{numberingKernel.sourceCommit.slice(0, 12)}</dd></div><div><dt></dt><dd>{numberingKernel.humanRouteNamespaces.join(' · ')}</dd></div><div><dt></dt><dd>{numberingKernel.automaticIdentityIssuance ? '已开启' : '禁止'}</dd></div><div><dt></dt><dd>{numberingKernel.unknownNumber === 'FAIL_CLOSED' ? '失败关闭 · 不猜测' : numberingKernel.unknownNumber}</dd></div></dl> : <p className="boundary-note"></p>}</section>