feat: embed cross-platform HoloLake terminal link
This commit is contained in:
parent
0be5ab3407
commit
ae7f883891
16 changed files with 808 additions and 122 deletions
|
|
@ -60,12 +60,14 @@ MCP 只保留为外部工具适配入口;它断开或编程 AI 重启时,活
|
|||
写车道仍留在 HoloLake 中。写车道不会因连接断开而自动释放,必须由持有恢复钥匙的同一车道
|
||||
显式释放并产生回执,下一条车道才能取得写权限。
|
||||
|
||||
当前已实现并注册账号级单写车道内核;外部本机代理与增量仓库对象通道仍处于后续迁入门,
|
||||
不得把本轮内核实现误报为外部编程 AI 已经完成直连。
|
||||
当前已实现账号级单写车道与外部编程 AI 终端直连协议。macOS、Linux 使用用户私有 Unix
|
||||
Socket,Windows 使用所有者与系统账户私有 Named Pipe;三者共用会话、心跳、断线续接、
|
||||
精确事件游标与工作环境事实帧。连接、续接、取得写车道及心跳都会让 HoloLake 返回当前
|
||||
运行所有者、写入者匹配、GLS 原生协议内核、有效期与摘要,模型不再负责通读协议正文恢复环境。
|
||||
尚未实现的是第二阶段的受监督 Shell、通用 Agent 工具循环、发布与部署执行器。
|
||||
|
||||
编程 AI 不允许把永久依赖 MCP 当成能力终态。若首次进入时缺少 HoloLake 本机直连器,它应在
|
||||
当前唯一写车道内补齐、测试并登记该连接器;经声明权限和人类批准后安装到 HoloLake,迁移为
|
||||
可认证、可恢复的本机直连。MCP 随后只保留为发现、恢复与兼容入口。
|
||||
编程 AI 不允许把永久依赖 MCP 当成能力终态。已安装 HoloLake 的 `--connector` 是可认证、
|
||||
可恢复的本机直连入口;MCP 只保留为发现、恢复与兼容入口。
|
||||
|
||||
## 当前收束与下一门
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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/)
|
||||
})
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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{}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 可像发现附近网络一样发现本机 HoloLake;MCP 仅用于兼容与恢复。</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} · P0–P6 共 {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>P1–P6 原生器官</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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue