diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/01-current-channel-overlap.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/01-current-channel-overlap.png new file mode 100644 index 000000000..1865fb4e5 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/01-current-channel-overlap.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/02-fixed-channel.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/02-fixed-channel.png new file mode 100644 index 000000000..1fce4b701 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/02-fixed-channel.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/03-external-ai-gateway.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/03-external-ai-gateway.png new file mode 100644 index 000000000..30c8f3372 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/03-external-ai-gateway.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/04-home-lighthouse.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/04-home-lighthouse.png new file mode 100644 index 000000000..8929e4696 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/04-home-lighthouse.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/05-public-main-domain.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/05-public-main-domain.png new file mode 100644 index 000000000..c30b23715 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/05-public-main-domain.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain-pre-contrast-fix.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain-pre-contrast-fix.png new file mode 100644 index 000000000..becfbf755 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain-pre-contrast-fix.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain.png new file mode 100644 index 000000000..dcb5fc8ad Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/06-public-branch-domain.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/07-public-zero-domain.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/07-public-zero-domain.png new file mode 100644 index 000000000..5b6c8eecd Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/07-public-zero-domain.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/08-home-css-lighthouse.png b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/08-home-css-lighthouse.png new file mode 100644 index 000000000..f22b6c41e Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/08-home-css-lighthouse.png differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/09-home-star-abyss.jpeg b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/09-home-star-abyss.jpeg new file mode 100644 index 000000000..4a1bc4d8f Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/09-home-star-abyss.jpeg differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/10-star-abyss-number-input.jpeg b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/10-star-abyss-number-input.jpeg new file mode 100644 index 000000000..f0dd103f0 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/10-star-abyss-number-input.jpeg differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/11-world-unfolded-after-number.jpeg b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/11-world-unfolded-after-number.jpeg new file mode 100644 index 000000000..d8b520539 Binary files /dev/null and b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/11-world-unfolded-after-number.jpeg differ diff --git a/product-source/hololake-native-desktop/audit/ui-overlap-20260819/README.md b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/README.md new file mode 100644 index 000000000..4867f9e74 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/ui-overlap-20260819/README.md @@ -0,0 +1,49 @@ +# HoloLake 单场景、公共入口与编号星渊验收记录 + +日期:2026-08-19 + +## 结论 + +- 旧、新首页重复拥有场景的根因已经关闭:五域湖面只保留一个可见场景所有者,频道、商城和天气使用互不复用的槽位。 +- 编号入口最终不是灯塔,也不是五域旁边的附属按钮。未验证首页只渲染湖面与大型未知星渊;产品名、光湖历、五域和频道凭证均不进入可访问树。 +- 点击星渊后,同一位置翻开为清晰的编号输入面;编号通过后星渊外翻消散,产品名与五湖分层升起,最后才出现频道凭证。 +- 主域、分域、零域是可进入的公共只读入口;第五域与零感域只公开职责和边界,不投影内部成员、仓库或私有内容。 +- 外部编程 AI 网关默认关闭,必须由已验证的人类在授权中心明确开启;MCP 只暴露登记过的只读发现、状态和能力清单。 +- 公共首页允许在未登录、尚未建立私人商城账本时启动。私人安装账本休眠,不再终止整个桌面应用。 + +## 可视证据 + +- `01-current-channel-overlap.png`:修复前的重复 UI。 +- `02-fixed-channel.png`:单一频道场景。 +- `03-external-ai-gateway.png`:默认关闭的真实外部 AI 网关。 +- `04-home-lighthouse.png`:被用户否决的胶囊形灯塔。 +- `05-public-main-domain.png`:主域公共入口。 +- `06-public-branch-domain.png`:分域双区商城公共入口。 +- `07-public-zero-domain.png`:零域协议运行投影。 +- `08-home-css-lighthouse.png`:再次被用户否决的机械灯塔方向,仅作纠错证据。 +- `09-home-star-abyss.jpeg`:最终未验证首页;只显示大型未知星渊。 +- `10-star-abyss-number-input.jpeg`:星渊翻开后的真实编号输入状态;底层入口不会重复残留。 +- `11-world-unfolded-after-number.jpeg`:真实编号通过后,平台标题、五湖和频道凭证才出现。 + +## 视觉自审 + +- 布局:验证前的大型星渊占据首页中部,与湖面地平线形成一个入口,不把它缩成图标。 +- 层级:验证前没有产品标题和五域竞争注意力;验证后才建立标题、光湖历、五湖和频道凭证层级。 +- 字体:沿用现有中文字体、字距和暖白标签,不引入新字体系统。 +- 色彩:星渊只使用湖面现有深蓝、冷紫雾光和少量内部星点,不新增机械实体色。 +- 控件:整个星渊仍是语义化 `button`;未验证时,隐藏世界不会泄露进辅助技术可访问树。 + +## 运行证据 + +- 前端生产构建通过。 +- JavaScript/合同测试全量通过。 +- Rust 测试全量通过。 +- macOS 桌面包通过 Developer ID 校验,标识为 `world.guanghu.hololake`。 +- 最终安装二进制 SHA-256:`17d20c67cca86dd3fc919726479446e8a05c65a99a98867a8438051e3490e9a1`。 +- 本地签名 App 已真实走通“星渊 → 编号验证 → 五湖升起 → 频道凭证”,并在验收后留在未验证星渊首页。 + +## 已知发布边界 + +- 本机 Developer ID 签名有效。 +- 本轮没有 Apple notarization 环境变量,因此未做在线公证。 +- Tauri updater 公钥已配置,但当前环境没有 `TAURI_SIGNING_PRIVATE_KEY`,所以没有生成可发布的签名增量更新包;这不影响本地 `.app` 运行。 diff --git a/product-source/hololake-native-desktop/contracts/external-ai-gateway.json b/product-source/hololake-native-desktop/contracts/external-ai-gateway.json new file mode 100644 index 000000000..76399ff06 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/external-ai-gateway.json @@ -0,0 +1,32 @@ +{ + "schema": "hololake.external-ai-gateway/v1", + "record_id": "HLP-EXTERNAL-AI-GATEWAY-001", + "state": "IMPLEMENTED_HUMAN_GATED", + "default_exposure": "CLOSED", + "human_authorization_required": true, + "mcp": { + "transport": "STDIO_JSON_RPC", + "protocol_version": "2025-06-18", + "role": "DISCOVERY_AND_CAPABILITY_CATALOG", + "persistent_continuity_owner": false + }, + "direct_protocol": { + "protocol": "HOLOLAKE_TERMINAL_LINK/3", + "transport": "USER_PRIVATE_LOCAL_SOCKET_OR_NAMED_PIPE", + "continuity_owner": "HOLOLAKE", + "switch_after_mcp_discovery": true + }, + "catalog": { + "physical_modules": "CALLABLE_ONLY_THROUGH_REGISTERED_NUMBERED_ROUTES", + "cognitive_skills": "READ_ONLY_RESOURCES_WITHOUT_EXECUTION_AUTHORITY", + "human_readable_names_required": true, + "machine_numbers_secondary": true + }, + "boundaries": { + "supervised_shell_execution": false, + "general_agent_tool_loop": false, + "transport_is_authority": false, + "mcp_is_continuity_owner": false, + "unknown_capability": "FAIL_CLOSED" + } +} diff --git a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json index b9e9507f0..743e793bc 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -81,7 +81,8 @@ "get_authorization_center", "get_marketplace_snapshot", "sync_marketplace_catalog", - "get_active_cognitive_skills" + "get_active_cognitive_skills", + "get_external_ai_gateway_status" ], "input_wrapper_aliases": [ "confirm_hololake_update_install", @@ -184,7 +185,8 @@ "restore_web_novel_chapter_version", "export_web_novel_author_delivery", "revoke_mobile_sync_device", - "decide_authorization_request" + "decide_authorization_request", + "set_external_ai_gateway_exposure" ], "direct_field_aliases": { "perform_code_repo_login": [ @@ -396,6 +398,11 @@ "module_number": "HLP-NIPC-MOD-0033", "target_number": "HLP-NIPC-TGT-0033", "internal_name": "online_marketplace" + }, + { + "module_number": "HLP-NIPC-MOD-0034", + "target_number": "HLP-NIPC-TGT-0034", + "internal_name": "external_ai_gateway" } ], "operations": [ @@ -2103,6 +2110,28 @@ "admission": "VERIFIED_HUMAN_ROUTE", "effect": "READ_OR_STATUS", "payload_schema": "hololake.numbered-ipc.payload/get_active_cognitive_skills/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0156", + "alias": "get_external_ai_gateway_status", + "handler": "external_ai_gateway::get_gateway_status", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0034", + "target_number": "HLP-NIPC-TGT-0034", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "payload_schema": "hololake.numbered-ipc.payload/get_external_ai_gateway_status/v1" + }, + { + "operation_number": "HLP-NIPC-OP-0157", + "alias": "set_external_ai_gateway_exposure", + "handler": "external_ai_gateway::set_gateway_exposure", + "channel_number": "HLP-NIPC-CH-0002", + "module_number": "HLP-NIPC-MOD-0034", + "target_number": "HLP-NIPC-TGT-0034", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "payload_schema": "hololake.numbered-ipc.payload/set_external_ai_gateway_exposure/v1" } ] } diff --git a/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-ui-gateway-public-world.md b/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-ui-gateway-public-world.md new file mode 100644 index 000000000..6bd870e7d --- /dev/null +++ b/product-source/hololake-native-desktop/docs/agent-training/2026-08-19-qwen-ui-gateway-public-world.md @@ -0,0 +1,47 @@ +# Qwen 只读审查训练记录:UI、外部 AI 网关与公共五域 + +日期:2026-08-19 + +## 训练方式边界 + +本次“训练”指把真实审查任务、发现、修正和验收回执沉淀为可复用的工程经验,不声称修改模型权重,也不允许 Agent 自行获得写入、部署、密钥或人格绑定权限。 + +- Agent:Qwen Code CLI 0.21.9 +- 模型:qwen3.7-plus +- 模式:只读审查、sandbox、无仓库写入、无远端推送、无密钥读取 +- 审查范围:公共五域入口、编号大门、外部编程 AI MCP 网关、账号隔离、系统授权边界 + +## 审查结论 + +- Critical:0 +- High:0 +- Medium:1 +- Low:2 + +## 发现与闭环 + +1. `M-1`:外部 AI 网关状态查询没有强制已验证用户路径。 + - 修正:状态查询与开关统一经过 `verified_user_route`。 +2. `L-1`:公共受保护域卡片包含内部成员编号。 + - 修正:第五域、零感域公共投影仅保留边界、可见性和访问状态;回归测试禁止内部编号重新出现。 +3. `L-2`:MCP 工具调用参数没有服务端拒绝非空参数。 + - 修正:登记的无参工具收到非空 `arguments` 时返回 `HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY`,并新增 Rust 回归测试。 +4. 本机真实启动验收额外发现:未登录时,私人在线商城账本会返回 `HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED` 并终止公共首页。 + - 修正:公共首页先启动,私人商城账本无账号时休眠;登录后再按账号隔离根启动。 + +## 被接受的工程规则 + +- 公共世界先于私人账号存在,但公共只读不等于获得安装或执行权限。 +- 状态读取也属于受保护的网关面,不能因为“只读”就绕开已验证用户。 +- 面向人的公共卡片必须显示可理解的名称和职责,不能泄露内部成员、仓库或哈希式机器标识。 +- MCP 是接入入口,不是授权来源;传输层不能替代编号、用户验证和人类授权。 +- 无账号是可预期启动状态,不应被当作应用级致命错误。 +- 编号验证不是五域旁边的一个功能入口:验证前世界保持遮蔽;编号通过后才投影产品名、五域和频道凭证。 + +## 被拒绝的路线 + +- 拒绝默认开启外部编程 AI 接口。 +- 拒绝让 MCP 工具获得 shell、写盘、部署或人格绑定能力。 +- 拒绝把公共页面渲染所需状态存进私人账号目录。 +- 拒绝把仓库克隆或任意代码执行当成模块商城安装协议。 +- 拒绝胶囊灯塔、实体灯塔和缩成图标的小黑洞;编号入口采用占据首页主体的大型未知星渊。 diff --git a/product-source/hololake-native-desktop/docs/ui-experience/EXP-20260819-110-NUMBER-GATE-VEILS-WORLD-UNTIL-RESOLVED.json b/product-source/hololake-native-desktop/docs/ui-experience/EXP-20260819-110-NUMBER-GATE-VEILS-WORLD-UNTIL-RESOLVED.json new file mode 100644 index 000000000..34c384587 --- /dev/null +++ b/product-source/hololake-native-desktop/docs/ui-experience/EXP-20260819-110-NUMBER-GATE-VEILS-WORLD-UNTIL-RESOLVED.json @@ -0,0 +1,30 @@ +{ + "experience_id": "EXP-20260819-110-NUMBER-GATE-VEILS-WORLD-UNTIL-RESOLVED", + "date": "2026-08-19", + "state": "IMPLEMENTED_AND_VISUALLY_VERIFIED", + "trigger": "冰朔连续否决胶囊灯塔、机械灯塔和缩成图标的小黑洞,并明确编号验证本身就是进入光湖语言世界的大门。", + "emergence": "未验证首页只保留湖面与大型未知星渊;点击后星渊翻开为编号输入;验证成功后星渊外翻消散,平台标题、五湖和频道凭证依次出现。", + "lock": [ + "编号未通过前,平台标题、光湖历、五域和频道凭证不得进入可见或可访问投影。", + "编号星渊必须是首页视觉主体,不得退化为角落按钮、胶囊卡片或机械实体。", + "编号输入面必须清晰,不得使用会让文字失焦的模糊入场。", + "编号成功后才允许五湖升起;账号凭证永远位于编号验证之后。" + ], + "why": "编号不是登录表单的装饰,而是语言世界是否向当前人类开放的系统边界。先暴露五域再验证,会把权限顺序画反;缩小入口则会把世界大门误画成普通功能按钮。", + "rejected": [ + "玻璃胶囊灯塔", + "尖顶机械灯塔", + "小型黑洞图标", + "验证前显示平台名或五域", + "独立弹层叠在仍可见的旧星渊之上", + "编号输入文字模糊过渡" + ], + "sources": [ + "冰朔 2026-08-19 本轮自然语言纠正", + "GHS-014 当前客户端 UI 思维大脑", + "ZY-AESTHETIC-COGNITION 系列", + "audit/ui-overlap-20260819/09-home-star-abyss.jpeg", + "audit/ui-overlap-20260819/10-star-abyss-number-input.jpeg", + "audit/ui-overlap-20260819/11-world-unfolded-after-number.jpeg" + ] +} diff --git a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json index 32faaa7a7..031864f75 100644 --- a/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json +++ b/product-source/hololake-native-desktop/generated/unified-number-coordinate-tree.json @@ -54,7 +54,7 @@ }, { "recordId": "HLP-NUMBERED-IPC-ROOT-001", - "sha256": "d6ddefe1c8f5cf5936367b38c251bf71c8b3cc683831572ea5bbeff23e5425fd" + "sha256": "11be5365a7a6d7d131af2c023b0401a1aa147f702ee125a4ddaf9f3171d1c4c2" }, { "recordId": "HLP-NBROKER-ROOT-001", @@ -76,8 +76,8 @@ "unresolvedNumberReferenceCount": 0, "mismatchedCoordinate": "FAIL_CLOSED" }, - "coordinateCount": 283, - "routeCount": 180, + "coordinateCount": 285, + "routeCount": 182, "identityNodeCount": 4, "protocolNodeCount": 99, "referenceOnlyNodeCount": 16, @@ -3248,6 +3248,34 @@ "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0033/HLP-NIPC-OP-0155/HLP-NIPC-TGT-0033" }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0034", + "operationNumber": "HLP-NIPC-OP-0156", + "targetNumber": "HLP-NIPC-TGT-0034", + "alias": "get_external_ai_gateway_status", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "READ_OR_STATUS", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0034/HLP-NIPC-OP-0156/HLP-NIPC-TGT-0034" + }, + { + "transport": "TAURI_WEBVIEW_NUMBERED_IPC", + "protocolVersion": "HLP-NIPC-v1", + "callerNumber": "HLP-NIPC-CALLER-MAIN-WEBVIEW-0001", + "channelNumber": "HLP-NIPC-CH-0002", + "moduleNumber": "HLP-NIPC-MOD-0034", + "operationNumber": "HLP-NIPC-OP-0157", + "targetNumber": "HLP-NIPC-TGT-0034", + "alias": "set_external_ai_gateway_exposure", + "admission": "VERIFIED_HUMAN_ROUTE", + "effect": "STATE_CHANGE", + "evidence": "HASH_CHAINED_NUMBERED_IPC_RECEIPT", + "path": "HLP-NUMBER-WORLD-ROOT-001/TAURI/HLP-NIPC-CH-0002/HLP-NIPC-MOD-0034/HLP-NIPC-OP-0157/HLP-NIPC-TGT-0034" + }, { "transport": "TAURI_WEBVIEW_NUMBERED_IPC", "protocolVersion": "HLP-NIPC-v1", diff --git a/product-source/hololake-native-desktop/scripts/channel-surface-layout.test.mjs b/product-source/hololake-native-desktop/scripts/channel-surface-layout.test.mjs new file mode 100644 index 000000000..2ac15c365 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/channel-surface-layout.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') + +test('channel surface gives every visible entry its own layout slot', () => { + const source = read('src/main.tsx') + const styles = read('src/styles.css') + + const channel = source.match(/\{worldStage === 'channel'[\s\S]*?\n \{worldStage === 'enterpriseWork'/)?.[0] + assert.ok(channel, 'channel world source must remain discoverable') + assert.match(channel, /className="channel-primary"/) + assert.match(channel, /className="channel-marketplace"/) + assert.equal((channel.match(/className="channel-main"/g) || []).length, 0) + + assert.match(styles, /\.channel-primary\s*\{/) + assert.match(styles, /\.channel-marketplace\s*\{/) +}) +test('the domain stage has exactly one atmosphere owner', () => { + const source = read('src/main.tsx') + assert.match( + source, + /\{!\(worldStage === 'domain' && surface === 'world'\) && \}/, + ) +}) diff --git a/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs b/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs index aa088dbbd..92ba7a1c2 100644 --- a/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs +++ b/product-source/hololake-native-desktop/scripts/domain-number-routing.test.mjs @@ -19,27 +19,30 @@ test('public routing starts from five domains and lets the number select the ver assert.equal(contract.registries.ENTERPRISE_FOUR_DOMAINS.ownership, 'TCS_0002_ENTERPRISE_REALITY_BODY') }) -test('the public shell shows five domains and login is domain-routed', () => { +test('the public shell keeps five domains behind number resolution and login is domain-routed', () => { const frontend = read('src/main.tsx') const login = read('src-tauri/src/code_repo_login.rs') const router = read('src-tauri/src/zero_point.rs') for (const name of ['光湖主域', '光湖分域', '光湖零域', '光湖零感域', '第五域 · 光湖本源域']) { assert.match(frontend, new RegExp(name.replace('·', '\\·'))) } - assert.match(frontend, /输入编号进入所属域/) + assert.match(frontend, /语言世界尚未展开/) + assert.match(frontend, /编号验证/) + assert.match(frontend, /gateRising \|\| gateStage === 'key'/) assert.match(frontend, /TCS-GL-/) - assert.match(frontend, /肥猫 · TCS-GL-0007∞/) - assert.match(frontend, /桔子 · TCS-GL-0008∞/) - assert.match(frontend, /烬舟 · PER-JZ001 · AGE/) - assert.match(frontend, /熹微 · PER-JZ-ARCH-001 · AGE/) + assert.match(frontend, /公共可见范围.*只公开域的存在与职责边界/s) + assert.match(frontend, /内部成员与工作仓库.*不在公共首页投影/s) + assert.doesNotMatch(frontend, /肥猫 · TCS-GL-0007∞/) + assert.doesNotMatch(frontend, /桔子 · TCS-GL-0008∞/) + assert.doesNotMatch(frontend, /烬舟 · PER-JZ001 · AGE/) + assert.doesNotMatch(frontend, /熹微 · PER-JZ-ARCH-001 · AGE/) assert.doesNotMatch(frontend, /肥猫 \+ 烬舟/) assert.match(frontend, /className="gate-close"/) assert.match(frontend, /className="domain-info-scrim"/) - assert.match(frontend, /setActiveDomainInfo\(gate\.domain\)/) + assert.match(frontend, /onDomain=\{\(domain\) => \{ if \(worldRevealed\) openPublicDomain\(domain\) \}\}/) assert.match(frontend, /event\.key !== 'Escape'/) - assert.match(frontend, /编号 \{zeroPoint\?\.userNumber \|\| gateNumber\} · 欢迎登入光湖语言世界/) + assert.match(frontend, /编号 \{gateNumber\} · 语言世界正在展开/) assert.match(frontend, /createWorldWelcome\(snapshot\)/) - assert.match(frontend, /className="world-impression"/) assert.match(frontend, /index === previous/) assert.match(frontend, /核对人格关系/) assert.match(frontend, /确认关系并签署/) @@ -66,7 +69,8 @@ test('the locked Qoder world template is the running client structure, not a ski assert.equal(contract.desktop_install_acceptance.mac_x86_64, 'NOT_BUILT') assert.equal(contract.desktop_install_acceptance.windows, 'NOT_BUILT') assert.equal(contract.desktop_install_acceptance.automatic_update_channel, 'PUBLIC_CHECK_PASS_NO_ACTIVE_RELEASE') - assert.match(frontend, /number-nucleus/) + assert.match(frontend, /star-abyss-dialog/) + assert.match(frontend, /worldRevealed/) assert.match(frontend, /worldStage === 'domain'/) assert.match(frontend, /worldStage === 'channel'/) assert.match(frontend, /worldStage === 'tool'/) diff --git a/product-source/hololake-native-desktop/scripts/external-ai-gateway.test.mjs b/product-source/hololake-native-desktop/scripts/external-ai-gateway.test.mjs new file mode 100644 index 000000000..8ef8bba16 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/external-ai-gateway.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') + +test('external programming AI gateway is human-gated and fails closed', () => { + const contract = JSON.parse(read('contracts/external-ai-gateway.json')) + assert.equal(contract.default_exposure, 'CLOSED') + assert.equal(contract.human_authorization_required, true) + assert.equal(contract.mcp.role, 'DISCOVERY_AND_CAPABILITY_CATALOG') + assert.equal(contract.direct_protocol.protocol, 'HOLOLAKE_TERMINAL_LINK/3') + assert.equal(contract.direct_protocol.continuity_owner, 'HOLOLAKE') + assert.equal(contract.boundaries.supervised_shell_execution, false) + assert.equal(contract.boundaries.transport_is_authority, false) +}) + +test('native binary exposes MCP stdio and numbered gateway controls', () => { + const main = read('src-tauri/src/main.rs') + const library = read('src-tauri/src/lib.rs') + const gateway = read('src-tauri/src/external_ai_gateway.rs') + const dispatcher = read('src-tauri/src/numbered_ipc_dispatch.rs') + const frontendIpc = read('src/modules/numbered-ipc.ts') + + assert.match(main, /--mcp/) + assert.match(library, /external_ai_gateway::run_mcp/) + assert.match(gateway, /initialize/) + assert.match(gateway, /tools\/list/) + assert.match(gateway, /tools\/call/) + assert.match(gateway, /hololake_discover/) + assert.match(gateway, /hololake_connection_status/) + assert.match(gateway, /hololake_registered_capabilities/) + assert.match(gateway, /HOLOLAKE_MCP_EXPOSURE_CLOSED/) + assert.match(gateway, /HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY/) + assert.match(dispatcher, /external_ai_gateway::get_gateway_status/) + assert.match(dispatcher, /external_ai_gateway::set_gateway_exposure/) + const statusRoute = dispatcher.match(/"external_ai_gateway::get_gateway_status"[\s\S]*?"external_ai_gateway::set_gateway_exposure"/)?.[0] + assert.ok(statusRoute, 'gateway status route must remain discoverable') + assert.match(statusRoute, /verified_user_route/) + assert.match(statusRoute, /HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED/) + assert.match(frontendIpc, /get_external_ai_gateway_status/) + assert.match(frontendIpc, /set_external_ai_gateway_exposure/) +}) + +test('human authorization screen renders real gateway and capability state', () => { + const source = read('src/modules/human-authorization-center.tsx') + assert.match(source, /允许本机编程 AI 发现 HoloLake/) + assert.match(source, /已注册技能/) + assert.match(source, /已接通集成/) + assert.match(source, /MCP 标准入口/) + assert.match(source, /HoloLake 本地直连协议/) +}) diff --git a/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs b/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs index a47bf09a5..21d857b9f 100644 --- a/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs +++ b/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs @@ -30,6 +30,10 @@ test('one online marketplace keeps physical modules and cognitive skills on sepa assert.match(runtime, /install_skill_at/) }) +test('public five-domain home can open before a private marketplace account exists', () => { + assert.match(runtime, /Err\(error\) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok\(\(\)\)/) +}) + test('marketplace lifecycle is reachable only through exact numbered operations', () => { const aliases = registry.operations.filter((operation) => operation.module_number === 'HLP-NIPC-MOD-0033').map((operation) => operation.alias) assert.deepEqual(aliases, [ diff --git a/product-source/hololake-native-desktop/scripts/public-world-entrances.test.mjs b/product-source/hololake-native-desktop/scripts/public-world-entrances.test.mjs new file mode 100644 index 000000000..7aae52d75 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/public-world-entrances.test.mjs @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8') + +test('number verification is an unknown star abyss and the world stays veiled until it resolves', () => { + const surface = read('src/modules/qoder-surface/StarlakeSurface.tsx') + const styles = read('src/modules/qoder-surface/starlake-surface.css') + const app = read('src/main.tsx') + + assert.match(surface, /worldRevealed/) + assert.match(surface, /worldRevealing/) + assert.match(surface, /gateExpanded/) + assert.match(surface, /className=\{`star-abyss-entry/) + assert.match(surface, /className="abyss-field"/) + assert.match(surface, /className="abyss-stars"/) + assert.match(surface, /编号验证/) + assert.match(surface, /验证后展开语言世界/) + assert.doesNotMatch(surface, /lighthouse-/) + assert.doesNotMatch(surface, /光湖灯塔/) + assert.doesNotMatch(surface, /className="channel-entry"/) + assert.match(styles, /\.starlake-scene\.world-veiled \.domains/) + assert.match(styles, /\.starlake-scene\.world-veiled \.masthead/) + assert.match(styles, /\.starlake-scene \.star-abyss-entry\s*\{/) + assert.match(styles, /left:50%/) + assert.match(styles, /@keyframes sl-domain-rise/) + assert.match(styles, /@keyframes sl-abyss-open/) + assert.match(app, /className="star-abyss-dialog"/) + assert.match(app, /语言世界尚未展开/) + assert.match(app, /gateRising \|\| gateStage === 'key'/) + assert.match(app, /gateExpanded=\{gateOpen && gateStage === 'number'\}/) +}) + +test('main branch and zero domains are public portals with real runtime projections', () => { + const surface = read('src/modules/qoder-surface/StarlakeSurface.tsx') + const app = read('src/main.tsx') + const portal = read('src/modules/public-domain/PublicDomainPortal.tsx') + + assert.match(surface, /publicAccess: true/) + assert.equal((surface.match(/publicAccess: true/g) || []).length, 3) + assert.match(app, /openPublicDomain/) + assert.match(app, / { + const app = read('src/main.tsx') + const portal = read('src/modules/public-domain/PublicDomainPortal.tsx') + const protectedFacts = app.match(/domain: 'ZERO_SENSE_DOMAIN'[\s\S]*?domain: 'FIFTH_DOMAIN'[\s\S]*?\n \] \},/)?.[0] + + assert.match(app, /ZERO_SENSE_DOMAIN.*setActiveDomainInfo/s) + assert.match(app, /FIFTH_DOMAIN.*setActiveDomainInfo/s) + assert.match(portal, /前三域公共只读入口/) + assert.match(portal, /不授予安装、写入或域内管理权限/) + assert.ok(protectedFacts, 'protected domain facts must remain discoverable') + assert.doesNotMatch(protectedFacts, /TCS-GL-|PER-|ICE-GL/) +}) diff --git a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs index d8e9bc9eb..3e1e8ffcb 100644 --- a/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs +++ b/product-source/hololake-native-desktop/scripts/unified-number-coordinate-tree.test.mjs @@ -8,13 +8,13 @@ test('identity, webview and direct broker numbers compile into one unique eviden assert.deepEqual(generated, compileUnifiedNumberTree()) assert.equal(generated.schema, 'hololake.unified-number-coordinate-tree/v2') assert.equal(generated.recordId, 'HLP-UNIFIED-NUMBER-TREE-001') - assert.equal(generated.coordinateCount, 283) - assert.equal(generated.routeCount, 180) + assert.equal(generated.coordinateCount, 285) + assert.equal(generated.routeCount, 182) assert.equal(generated.identityNodeCount, 4) assert.equal(generated.protocolNodeCount, 99) assert.equal(generated.referenceOnlyNodeCount, 16) - assert.equal(new Set(generated.routes.map((route) => route.path)).size, 180) - assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 283) + assert.equal(new Set(generated.routes.map((route) => route.path)).size, 182) + assert.equal(new Set([...generated.identityNodes, ...generated.protocolNodes, ...generated.routes].map((node) => node.path)).size, 285) assert.equal(generated.invariants.everyPhysicalCallHasNumberedRoute, true) assert.equal(generated.invariants.everyAcceptedCallHasEvidenceClass, true) assert.equal(generated.invariants.everyProtocolReferenceHasNumberCoordinate, true) diff --git a/product-source/hololake-native-desktop/src-tauri/src/external_ai_gateway.rs b/product-source/hololake-native-desktop/src-tauri/src/external_ai_gateway.rs new file mode 100644 index 000000000..1df1a7369 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/src/external_ai_gateway.rs @@ -0,0 +1,538 @@ +//! Human-gated MCP discovery surface for external programming AIs. +//! +//! MCP exposes discovery and readable capability metadata only. Durable sessions, +//! environment frames and any later mutation stay on HOLOLAKE_TERMINAL_LINK/3. + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::fs::{self, OpenOptions}; +use std::io::{self, BufRead, Write}; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::{AppHandle, Manager}; +use uuid::Uuid; + +const CONFIG_NAME: &str = "external-ai-gateway-v1.json"; +const CAPABILITY_CACHE_NAME: &str = "external-ai-capabilities-v1.json"; +const BROKER_DESCRIPTOR_NAME: &str = "direct-local-broker-v1.json"; +const MCP_PROTOCOL_VERSION: &str = "2025-06-18"; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SetGatewayExposureInput { + pub enabled: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct GatewayConfig { + schema: String, + enabled: bool, + authorized_by_human_number: String, + updated_at_unix_ms: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GatewaySkill { + pub name: String, + pub summary: String, + pub version: String, + pub state: String, + pub read_only: bool, + pub execution_authority: bool, + pub triggers: Vec, + pub method: Vec, + pub constraints: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GatewayIntegration { + pub name: String, + pub summary: String, + pub state: String, + pub exposed: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExternalAiGatewayStatus { + pub schema: String, + pub state: String, + pub exposure: String, + pub human_authorization_required: bool, + pub mcp_transport: String, + pub mcp_protocol_version: String, + pub mcp_command: String, + pub direct_protocol: String, + pub direct_connector_command: String, + pub broker_state: String, + pub registered_skill_count: usize, + pub active_skill_count: usize, + pub connected_integration_count: usize, + pub integrations: Vec, + pub skills: Vec, + pub observed_at_unix_ms: u64, +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as u64) + .unwrap_or(0) +} + +fn gateway_root() -> Result { + if let Some(path) = std::env::var_os("HOLOLAKE_EXTERNAL_AI_GATEWAY_ROOT") { + return Ok(PathBuf::from(path)); + } + dirs::data_dir() + .map(|root| root.join("world.guanghu.hololake")) + .ok_or_else(|| "HOLOLAKE_APP_DATA_UNAVAILABLE".to_string()) +} + +fn root_for_app(app: &AppHandle) -> Result { + app.path() + .app_data_dir() + .map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}")) +} + +fn config_path(root: &Path) -> PathBuf { + root.join(CONFIG_NAME) +} + +fn capability_cache_path(root: &Path) -> PathBuf { + root.join(CAPABILITY_CACHE_NAME) +} + +fn read_config(root: &Path) -> Result { + let path = config_path(root); + if !path.exists() { + return Ok(GatewayConfig { + schema: "hololake.external-ai-gateway-config/v1".into(), + enabled: false, + authorized_by_human_number: String::new(), + updated_at_unix_ms: 0, + }); + } + let config: GatewayConfig = serde_json::from_slice( + &fs::read(path).map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_READ_FAILED: {error}"))?, + ) + .map_err(|error| format!("HOLOLAKE_GATEWAY_CONFIG_INVALID: {error}"))?; + if config.schema != "hololake.external-ai-gateway-config/v1" { + return Err("HOLOLAKE_GATEWAY_CONFIG_UNSUPPORTED".into()); + } + Ok(config) +} + +fn write_json_atomic(path: &Path, value: &T) -> Result<(), String> { + let parent = path.parent().ok_or("HOLOLAKE_GATEWAY_PATH_INVALID")?; + fs::create_dir_all(parent).map_err(|error| format!("HOLOLAKE_GATEWAY_DIR_FAILED: {error}"))?; + let temporary = parent.join(format!(".gateway-{}.tmp", Uuid::new_v4())); + let bytes = serde_json::to_vec_pretty(value) + .map_err(|error| format!("HOLOLAKE_GATEWAY_SERIALIZE_FAILED: {error}"))?; + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + options.mode(0o600); + let mut file = options + .open(&temporary) + .map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?; + file.write_all(&bytes) + .and_then(|_| file.sync_all()) + .map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}"))?; + fs::rename(&temporary, path).map_err(|error| format!("HOLOLAKE_GATEWAY_WRITE_FAILED: {error}")) +} + +fn executable_commands() -> (String, String) { + let executable = std::env::current_exe() + .map(|path| path.to_string_lossy().into_owned()) + .unwrap_or_else(|_| "HoloLake".into()); + ( + format!("{} --mcp", shell_display(&executable)), + format!("{} --connector", shell_display(&executable)), + ) +} + +fn shell_display(value: &str) -> String { + if value.contains(' ') { + format!("\"{}\"", value.replace('"', "\\\"")) + } else { + value.into() + } +} + +fn broker_state(root: &Path) -> String { + let descriptor = fs::read(root.join(BROKER_DESCRIPTOR_NAME)) + .ok() + .and_then(|raw| serde_json::from_slice::(&raw).ok()); + match descriptor + .as_ref() + .and_then(|value| value.get("state")) + .and_then(Value::as_str) + { + Some("LISTENING") => "READY".into(), + _ => "WAITING_FOR_LOGIN".into(), + } +} + +pub async fn get_gateway_status(app: AppHandle) -> Result { + let root = root_for_app(&app)?; + let config = read_config(&root)?; + let marketplace = crate::online_marketplace::get_marketplace_snapshot(app.clone()) + .await + .unwrap_or_default(); + let active = crate::online_marketplace::get_active_cognitive_skills(app) + .await + .unwrap_or_default(); + let active_by_number = active + .into_iter() + .map(|skill| (skill.skill_number.clone(), skill)) + .collect::>(); + let skills = marketplace + .items + .iter() + .filter(|item| item.artifact_kind == "COGNITIVE_SKILL") + .map(|item| { + let active = active_by_number.get(&item.item_number); + GatewaySkill { + name: item.display_name.clone(), + summary: item.summary.clone(), + version: item.version.clone(), + state: item.installed_state.clone(), + read_only: true, + execution_authority: false, + triggers: active + .map(|value| value.payload.triggers.clone()) + .unwrap_or_default(), + method: active + .map(|value| value.payload.method.clone()) + .unwrap_or_default(), + constraints: active + .map(|value| value.payload.constraints.clone()) + .unwrap_or_default(), + } + }) + .collect::>(); + let exposure = if config.enabled { "OPEN" } else { "CLOSED" }; + let broker = broker_state(&root); + let integrations = vec![ + GatewayIntegration { + name: "MCP 标准入口".into(), + summary: "供外部编程 AI 发现 HoloLake 与读取已注册能力;不承载长期上下文。".into(), + state: if config.enabled { + "已开放" + } else { + "已关闭" + } + .into(), + exposed: config.enabled, + }, + GatewayIntegration { + name: "HoloLake 本地直连协议".into(), + summary: "MCP 发现后切换到本机私有连接,持续会话与环境回执由 HoloLake 承载。".into(), + state: if broker == "READY" { + "已就绪" + } else { + "等待登录" + } + .into(), + exposed: config.enabled && broker == "READY", + }, + GatewayIntegration { + name: "编号 IPC 授权桥".into(), + summary: "前端与本机能力只通过完整编号坐标通信,未知路径关闭。".into(), + state: "已接通".into(), + exposed: true, + }, + GatewayIntegration { + name: "线上模块与技能商城".into(), + summary: "展示双签目录中的成品模块和只读思维技能。".into(), + state: if marketplace.state == "ACTIVE_VERIFIED_CATALOG" { + "目录已验证" + } else { + "等待目录同步" + } + .into(), + exposed: marketplace.state == "ACTIVE_VERIFIED_CATALOG", + }, + ]; + let (mcp_command, direct_connector_command) = executable_commands(); + let status = ExternalAiGatewayStatus { + schema: "hololake.external-ai-gateway-status/v1".into(), + state: "HUMAN_GATED".into(), + exposure: exposure.into(), + human_authorization_required: true, + mcp_transport: "STDIO_JSON_RPC".into(), + mcp_protocol_version: MCP_PROTOCOL_VERSION.into(), + mcp_command, + direct_protocol: "HOLOLAKE_TERMINAL_LINK/3".into(), + direct_connector_command, + broker_state: broker, + registered_skill_count: skills.len(), + active_skill_count: skills + .iter() + .filter(|skill| skill.state == "ACTIVE_READONLY") + .count(), + connected_integration_count: integrations.iter().filter(|item| item.exposed).count(), + integrations, + skills, + observed_at_unix_ms: now_unix_ms(), + }; + write_json_atomic(&capability_cache_path(&root), &status)?; + Ok(status) +} + +pub async fn set_gateway_exposure( + app: AppHandle, + input: SetGatewayExposureInput, + human_number: &str, +) -> Result { + let root = root_for_app(&app)?; + write_json_atomic( + &config_path(&root), + &GatewayConfig { + schema: "hololake.external-ai-gateway-config/v1".into(), + enabled: input.enabled, + authorized_by_human_number: human_number.into(), + updated_at_unix_ms: now_unix_ms(), + }, + )?; + get_gateway_status(app).await +} + +fn load_cached_status(root: &Path) -> Result { + serde_json::from_slice( + &fs::read(capability_cache_path(root)) + .map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_UNAVAILABLE: {error}"))?, + ) + .map_err(|error| format!("HOLOLAKE_GATEWAY_CAPABILITY_CACHE_INVALID: {error}")) +} + +fn jsonrpc_result(id: Value, result: Value) -> Value { + json!({"jsonrpc": "2.0", "id": id, "result": result}) +} + +fn jsonrpc_error(id: Value, code: i64, message: &str) -> Value { + json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}}) +} + +fn tool_result(value: Value) -> Value { + json!({ + "content": [{"type": "text", "text": serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".into())}], + "isError": false + }) +} + +fn tools_list() -> Value { + json!({"tools": [ + {"name": "hololake_discover", "description": "发现本机 HoloLake,并取得切换到稳定直连协议的方法。", "inputSchema": {"type": "object", "additionalProperties": false}}, + {"name": "hololake_connection_status", "description": "读取 MCP 开放状态与 HoloLake 本地直连入口状态。", "inputSchema": {"type": "object", "additionalProperties": false}}, + {"name": "hololake_registered_capabilities", "description": "读取人类可理解的官方技能与集成目录;只读技能不获得现实执行权限。", "inputSchema": {"type": "object", "additionalProperties": false}} + ]}) +} + +fn resources_list(status: &ExternalAiGatewayStatus) -> Value { + json!({"resources": status.skills.iter().enumerate().map(|(index, skill)| json!({ + "uri": format!("hololake://skills/{index}"), + "name": skill.name, + "description": skill.summary, + "mimeType": "application/json" + })).collect::>()}) +} + +fn handle_mcp_request(root: &Path, request: &Value) -> Option { + let id = request.get("id").cloned(); + let method = request + .get("method") + .and_then(Value::as_str) + .unwrap_or_default(); + if id.is_none() { + return None; + } + let id = id.unwrap_or(Value::Null); + let result = match method { + "initialize" => json!({ + "protocolVersion": MCP_PROTOCOL_VERSION, + "capabilities": {"tools": {"listChanged": false}, "resources": {"subscribe": false, "listChanged": false}}, + "serverInfo": {"name": "HoloLake", "version": env!("CARGO_PKG_VERSION")}, + "instructions": "MCP 只负责发现与能力目录。持续协作请切换到 HOLOLAKE_TERMINAL_LINK/3;任何执行仍受人类授权与编号路由约束。" + }), + "ping" => json!({}), + "tools/list" => tools_list(), + "tools/call" => { + if let Some(arguments) = request.pointer("/params/arguments") { + let empty_object = arguments + .as_object() + .map(|value| value.is_empty()) + .unwrap_or(false); + if !empty_object { + return Some(jsonrpc_error( + id, + -32602, + "HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY", + )); + } + } + let name = request + .pointer("/params/name") + .and_then(Value::as_str) + .unwrap_or_default(); + let status = match load_cached_status(root) { + Ok(status) => status, + Err(error) => return Some(jsonrpc_error(id, -32002, &error)), + }; + match name { + "hololake_discover" => tool_result(json!({ + "service": "HoloLake", + "mcpRole": "DISCOVERY_AND_CAPABILITY_CATALOG", + "nextProtocol": status.direct_protocol, + "directConnectorCommand": status.direct_connector_command, + "continuityOwner": "HOLOLAKE", + "executionAuthorityGranted": false + })), + "hololake_connection_status" => { + tool_result(serde_json::to_value(&status).unwrap_or(Value::Null)) + } + "hololake_registered_capabilities" => tool_result( + json!({"integrations": status.integrations, "skills": status.skills}), + ), + _ => return Some(jsonrpc_error(id, -32602, "HOLOLAKE_MCP_TOOL_UNKNOWN")), + } + } + "resources/list" => match load_cached_status(root) { + Ok(status) => resources_list(&status), + Err(error) => return Some(jsonrpc_error(id, -32002, &error)), + }, + "resources/read" => { + let uri = request + .pointer("/params/uri") + .and_then(Value::as_str) + .unwrap_or_default(); + let index = uri + .strip_prefix("hololake://skills/") + .and_then(|value| value.parse::().ok()); + let status = match load_cached_status(root) { + Ok(status) => status, + Err(error) => return Some(jsonrpc_error(id, -32002, &error)), + }; + let Some(skill) = index.and_then(|index| status.skills.get(index)) else { + return Some(jsonrpc_error(id, -32003, "HOLOLAKE_MCP_RESOURCE_UNKNOWN")); + }; + json!({"contents": [{"uri": uri, "mimeType": "application/json", "text": serde_json::to_string_pretty(skill).unwrap_or_else(|_| "{}".into())}]}) + } + _ => return Some(jsonrpc_error(id, -32601, "HOLOLAKE_MCP_METHOD_UNKNOWN")), + }; + Some(jsonrpc_result(id, result)) +} + +pub fn run_mcp() -> Result<(), String> { + let root = gateway_root()?; + if !read_config(&root)?.enabled { + return Err("HOLOLAKE_MCP_EXPOSURE_CLOSED".into()); + } + let stdin = io::stdin(); + let mut stdout = io::stdout().lock(); + for line in stdin.lock().lines() { + let line = line.map_err(|error| format!("HOLOLAKE_MCP_INPUT_FAILED: {error}"))?; + if line.trim().is_empty() { + continue; + } + let request: Value = match serde_json::from_str(&line) { + Ok(value) => value, + Err(_) => { + serde_json::to_writer( + &mut stdout, + &jsonrpc_error(Value::Null, -32700, "Parse error"), + ) + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + stdout + .write_all(b"\n") + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + stdout + .flush() + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + continue; + } + }; + if let Some(response) = handle_mcp_request(&root, &request) { + serde_json::to_writer(&mut stdout, &response) + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + stdout + .write_all(b"\n") + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + stdout + .flush() + .map_err(|error| format!("HOLOLAKE_MCP_OUTPUT_FAILED: {error}"))?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn missing_config_fails_closed() { + let root = TempDir::new().unwrap(); + assert!(!read_config(root.path()).unwrap().enabled); + } + + #[test] + fn unknown_mcp_tool_fails_closed() { + let root = TempDir::new().unwrap(); + let status = ExternalAiGatewayStatus { + schema: "hololake.external-ai-gateway-status/v1".into(), + state: "HUMAN_GATED".into(), + exposure: "OPEN".into(), + human_authorization_required: true, + mcp_transport: "STDIO_JSON_RPC".into(), + mcp_protocol_version: MCP_PROTOCOL_VERSION.into(), + mcp_command: "HoloLake --mcp".into(), + direct_protocol: "HOLOLAKE_TERMINAL_LINK/3".into(), + direct_connector_command: "HoloLake --connector".into(), + broker_state: "READY".into(), + registered_skill_count: 0, + active_skill_count: 0, + connected_integration_count: 0, + integrations: vec![], + skills: vec![], + observed_at_unix_ms: 1, + }; + write_json_atomic(&capability_cache_path(root.path()), &status).unwrap(); + let response = handle_mcp_request( + root.path(), + &json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"shell"}}), + ) + .unwrap(); + assert_eq!( + response.pointer("/error/message").and_then(Value::as_str), + Some("HOLOLAKE_MCP_TOOL_UNKNOWN") + ); + } + + #[test] + fn tool_arguments_fail_closed() { + let root = TempDir::new().unwrap(); + let response = handle_mcp_request( + root.path(), + &json!({ + "jsonrpc":"2.0", + "id":1, + "method":"tools/call", + "params":{"name":"hololake_discover","arguments":{"shell":true}} + }), + ) + .unwrap(); + assert_eq!( + response.pointer("/error/message").and_then(Value::as_str), + Some("HOLOLAKE_MCP_TOOL_ARGUMENTS_NOT_EMPTY") + ); + } +} diff --git a/product-source/hololake-native-desktop/src-tauri/src/lib.rs b/product-source/hololake-native-desktop/src-tauri/src/lib.rs index e34350b4c..bf457c966 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/lib.rs @@ -10,6 +10,7 @@ mod dynamic_capability_routing; mod education_translation; mod education_workspace; mod enterprise_work_channel; +mod external_ai_gateway; mod glp_envelope; mod gls_bootstrap_compiler; mod gls_protocol_kernel; @@ -53,6 +54,10 @@ pub fn run_connector() -> Result<(), String> { direct_local_broker::run_connector() } +pub fn run_mcp() -> Result<(), String> { + external_ai_gateway::run_mcp() +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() diff --git a/product-source/hololake-native-desktop/src-tauri/src/main.rs b/product-source/hololake-native-desktop/src-tauri/src/main.rs index 4b4267e00..90d24c8cc 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/main.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/main.rs @@ -1,6 +1,13 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + if std::env::args().any(|argument| argument == "--mcp") { + if let Err(error) = hololake_native_desktop_lib::run_mcp() { + eprintln!("{error}"); + std::process::exit(1); + } + return; + } if std::env::args().any(|argument| argument == "--connector") { if let Err(error) = hololake_native_desktop_lib::run_connector() { eprintln!("{error}"); diff --git a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs index 0d57cdefa..211b311bd 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/number_coordinate_tree.rs @@ -88,8 +88,8 @@ fn validate_tree() -> Result<(), String> { || tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001" || tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED" || tree.root_number != "HLP-NUMBER-WORLD-ROOT-001" - || tree.coordinate_count != 283 - || tree.route_count != 180 + || tree.coordinate_count != 285 + || tree.route_count != 182 || tree.routes.len() != tree.route_count || tree.identity_node_count != 4 || tree.identity_nodes.len() != tree.identity_node_count diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs index 3b37d9055..a3d4a6a14 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc.rs @@ -934,7 +934,7 @@ mod tests { #[test] fn registry_is_closed_and_contains_every_migrated_command() { let registry = load_registry().unwrap(); - assert_eq!(registry.operations.len(), 155); + assert_eq!(registry.operations.len(), 157); assert!(!registry.runtime.legacy_direct_commands_allowed); } diff --git a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs index 7d385d7e7..7322f6932 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/numbered_ipc_dispatch.rs @@ -56,6 +56,25 @@ pub(crate) async fn dispatch( &human_number, )?) } + "external_ai_gateway::get_gateway_status" => { + let state = app.state::(); + crate::zero_point::verified_user_route(&state)? + .ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?; + json(crate::external_ai_gateway::get_gateway_status(app).await?) + } + "external_ai_gateway::set_gateway_exposure" => { + let state = app.state::(); + let (human_number, _) = crate::zero_point::verified_user_route(&state)? + .ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?; + json( + crate::external_ai_gateway::set_gateway_exposure( + app, + input(&payload)?, + &human_number, + ) + .await?, + ) + } "release_update::check_hololake_update" => { json(crate::release_update::check_hololake_update(app).await?) } diff --git a/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs b/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs index c455f4da5..8ee3d5dfd 100644 --- a/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs +++ b/product-source/hololake-native-desktop/src-tauri/src/online_marketplace.rs @@ -1482,8 +1482,16 @@ pub async fn get_active_cognitive_skills( pub fn start_on_application_open(app: &AppHandle) -> Result<(), String> { validate_embedded_contract()?; - let _ = open_db(&runtime_root(app)?)?; - Ok(()) + match runtime_root(app) { + Ok(root) => { + let _ = open_db(&root)?; + Ok(()) + } + // 公共五域首页与公共商城目录先于私人频道登录存在;这里只让账号隔离的 + // 安装账本休眠,不能因为没有私人账号就终止整个桌面应用。 + Err(error) if error == "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED" => Ok(()), + Err(error) => Err(error), + } } #[cfg(test)] diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 3127b2863..952235fad 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -9,6 +9,7 @@ import { FINISHES, TraditionalSurface, type FinishId, type TraditionalBroadcast, import { resolveVisualBalance } from './modules/qoder-surface/visual-balance' import { PrivateChannelSurface, type InstalledChannelModule, type PrivateChannelAction } from './modules/private-channel/PrivateChannelSurface' import { HumanAuthorizationCenter, type DirectSessionProjection } from './modules/human-authorization-center' +import { PublicDomainPortal, type PublicDomainId } from './modules/public-domain/PublicDomainPortal' const ChannelWorkbenchStudio = lazy(() => import('./modules/channel-workbench').then((module) => ({ default: module.ChannelWorkbenchStudio }))) const PersonaChannelBody = lazy(() => import('./modules/persona-channel-body').then((module) => ({ default: module.PersonaChannelBody }))) @@ -453,10 +454,10 @@ const domainGates = [ ['公共作用', '热更新实验 · 系统架构 · 语言推理模拟'], ['域标识', 'ZERO_DOMAIN'], ['责任主体', '页页 · TCS-GL-0006∞'], ['人格体主体', '页骨 · PER-YG001 · AGE'], ['关系支持', '小坍缩核 · PER-XTK001 · AGE'], ['工作仓库', 'PRIVATE · 1 · LIVE'], ] }, { domain: 'ZERO_SENSE_DOMAIN', className: 'd-zs', title: '光湖零感域', gate: 'GATE 04 · ONLINE', facts: [ - ['开放边界', '人类主控团队内部管理域 · 不对公众开放'], ['域标识', 'ZERO_SENSE_DOMAIN'], ['责任主体 01', '肥猫 · TCS-GL-0007∞'], ['人格体主体 01', '烬舟 · PER-JZ001 · AGE'], ['责任主体 02', '桔子 · TCS-GL-0008∞'], ['人格体主体 02', '熹微 · PER-JZ-ARCH-001 · AGE'], ['工作仓库', 'PRIVATE · 2 · LIVE'], + ['开放边界', '人类主控团队内部管理域 · 不对公众开放'], ['域标识', 'ZERO_SENSE_DOMAIN'], ['公共可见范围', '只公开域的存在与职责边界'], ['内部成员与工作仓库', '不在公共首页投影'], ['访问状态', 'PRIVATE · CLOSED'], ] }, { domain: 'FIFTH_DOMAIN', className: 'd-fifth', title: '第五域 · 光湖本源域', gate: 'GATE 05 · ONLINE', facts: [ - ['授权边界', '私有自由部署 · 逆向访问须经 ICE-GL∞ 编号授权'], ['域标识', 'FIFTH_DOMAIN'], ['责任主体', '冰朔 · ICE-GL∞'], ['系统入口', '永恒湖心系统'], ['访问状态', 'PRIVATE · LIVE'], + ['授权边界', '私有自由部署 · 逆向访问必须持有明确编号授权'], ['域标识', 'FIFTH_DOMAIN'], ['公共可见范围', '只公开域的存在与访问边界'], ['系统入口', '验证通过后进入所属私人系统'], ['访问状态', 'PRIVATE · NUMBER GATED'], ] }, ] const starPoints = [ @@ -494,6 +495,14 @@ function LakePool({ className, title, meta, open = false, risen = false, onClick } +function WorldThemeMenu({ theme, onSelect }: { theme: ThemeId; onSelect: (theme: ThemeId) => void }) { + const active = themes.find((item) => item.id === theme) || themes[0] + return
+ 主题 · {active.name} +
{themes.map((choice) =>