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 ef6b5c609..fbf9a127c 100644 --- a/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json +++ b/product-source/hololake-native-desktop/contracts/numbered-ipc-registry.json @@ -20,6 +20,7 @@ "payload_bound_grants": true, "authority_binding_issued_server_side": true, "verified_human_binding_required_for_protected_routes": true, + "ordinary_user_local_channel_authority": "EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET", "persona_binding_claimed": false, "user_channel_body_binding_stage": "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED", "maximum_grant_ttl_ms": 30000, 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 249ab05e6..f693aba8f 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": "ca5b07eb3efa897bc4309ecb1ad3c88824ed7a84d9318898777ae187a34155fb" + "sha256": "0498ff3d41a9311f28c0c705ad633072f075627e6c001baf954179ab41136166" }, { "recordId": "HLP-NBROKER-ROOT-001", 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 index 6c4f22d80..6c6ddb969 100644 --- a/product-source/hololake-native-desktop/scripts/external-ai-gateway.test.mjs +++ b/product-source/hololake-native-desktop/scripts/external-ai-gateway.test.mjs @@ -40,7 +40,7 @@ test('native binary exposes MCP stdio and numbered gateway controls', () => { 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, /authenticated_human_subject/) assert.match(statusRoute, /HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED/) assert.match(frontendIpc, /get_external_ai_gateway_status/) assert.match(frontendIpc, /set_external_ai_gateway_exposure/) diff --git a/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs index 99146b91e..9ec4deb14 100644 --- a/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs +++ b/product-source/hololake-native-desktop/scripts/numbered-ipc-root.test.mjs @@ -33,6 +33,7 @@ test('numbered IPC registry is a closed unique route graph', async () => { assert.equal(contract.runtime.grant_single_use, true) assert.equal(contract.runtime.payload_bound_grants, true) assert.equal(contract.runtime.authority_binding_issued_server_side, true) + assert.equal(contract.runtime.ordinary_user_local_channel_authority, 'EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET') assert.equal(contract.runtime.persona_binding_claimed, false) assert.equal(contract.runtime.unknown_route, 'FAIL_CLOSED') assert.ok(contract.operations.length >= 60) @@ -65,6 +66,19 @@ test('numbered IPC registry is a closed unique route graph', async () => { assert.deepEqual([...payloadAliases].sort(), [...aliases].sort()) }) +test('ordinary user local channels receive account-scoped IPC authority without claiming a Guanghu number', async () => { + const source = await readFile(path.join(rustRoot, 'numbered_ipc.rs'), 'utf8') + const dispatcher = await readFile(dispatcherPath, 'utf8') + assert.match(source, /AUTHENTICATED_LOCAL_HUMAN:PERSONAL_CHANNEL/) + assert.match(source, /session\.domain == "PERSONAL_CHANNEL"/) + assert.match(source, /session\.host == "local\.hololake"/) + assert.match(source, /current_login_session\(app\)/) + assert.match(source, /HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED/) + assert.match(dispatcher, /fn authenticated_human_subject/) + assert.match(dispatcher, /identity_for_channel_runtime\(app\)/) + assert.match(dispatcher, /external_ai_gateway::get_gateway_status[\s\S]*authenticated_human_subject/) +}) + test('the webview has one IPC entrance and cannot invoke legacy commands', async () => { const contract = JSON.parse(await readFile(contractPath, 'utf8')) const aliases = new Set(contract.operations.map((operation) => operation.alias)) 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 21d857b9f..1d6afc1b1 100644 --- a/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs +++ b/product-source/hololake-native-desktop/scripts/online-marketplace.test.mjs @@ -48,6 +48,13 @@ test('marketplace lifecycle is reachable only through exact numbered operations' assert.match(frontend, /只有零点原核与企业发布双签同时通过/) }) +test('an installed physical marketplace module becomes a directly usable ordinary-channel application', () => { + assert.match(frontend, /installMarketplaceItem[\s\S]*await refreshCompositionModule\(\)/) + assert.match(frontend, /installMarketplaceItem[\s\S]*await refreshEducationModule\(\)/) + assert.match(frontend, /compositionModule\?\.installedState === 'ACTIVE'[\s\S]*title="原生组合视图"[\s\S]*已安装成品模块 · 直接使用/) + assert.match(frontend, /educationModule\?\.installedState === 'ACTIVE'[\s\S]*title="教育行业工作台"[\s\S]*已安装成品模块 · 直接使用/) +}) + for (const file of [ 'HLP-SKILL-OFFICIAL-DELIVERY-VERIFICATION-0001-0.1.0.ghskill', 'HLP-SKILL-OFFICIAL-MODULE-BOUNDARY-REVIEW-0001-0.1.0.ghskill', diff --git a/product-source/hololake-native-desktop/scripts/persona-agent-runtime.test.mjs b/product-source/hololake-native-desktop/scripts/persona-agent-runtime.test.mjs index 0135bdfb0..852b285ad 100644 --- a/product-source/hololake-native-desktop/scripts/persona-agent-runtime.test.mjs +++ b/product-source/hololake-native-desktop/scripts/persona-agent-runtime.test.mjs @@ -31,8 +31,11 @@ test('knowledge-embedded Agent keeps one channel body with human-managed convers assert.equal(contract.channel.response_target_is_selected_per_turn, true) assert.equal(contract.channel.persona_identity_preconfigured, false) assert.equal(contract.channel.persona_declares_own_number_and_name, true) - assert.match(frontend, /ICE-GL∞/) - assert.match(frontend, /零点原核频道系统/) + assert.match(frontend, /正在读取当前频道/) + assert.match(frontend, /频道编号核验中/) + assert.match(frontend, /setRuntime\(nextRuntime\)[\s\S]*list_persona_agent_conversations/) + assert.doesNotMatch(frontend, /artifactCount \?\? 0/) + assert.doesNotMatch(frontend, /runtime\?\.humanNumber \|\| 'ICE-GL∞'/) assert.doesNotMatch(frontend, /ICE-P-ZY001/) assert.doesNotMatch(frontend, />\s*人类\s* { + const app = read('src/main.tsx') + const chooser = app.match(/const chooseSurface = \(next:[\s\S]*?window\.localStorage\.setItem\('hololake-surface', next\)\n \}/)?.[0] + assert.ok(chooser) + assert.match(chooser, /setWorldStage\('domain'\)/) + assert.match(chooser, /setView\('overview'\)/) + assert.match(chooser, /setPublicDomain\(null\)/) +}) + +test('language-world themes alter the whole scene through semantic optical tokens', () => { + const styles = read('src/modules/qoder-surface/starlake-surface.css') + assert.match(styles, /background:linear-gradient\(180deg, var\(--surface-sky\)/) + assert.match(styles, /opacity:var\(--primitive-scene-opacity\)/) + assert.match(styles, /background:var\(--accent-light\)/) + assert.match(styles, /color:var\(--content-primary\)/) + assert.match(styles, /color:var\(--content-secondary\)/) +}) + test('zero-sense and fifth-domain boundaries remain explicit', () => { const app = read('src/main.tsx') const portal = read('src/modules/public-domain/PublicDomainPortal.tsx') 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 a270992db..d9f65ca33 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 @@ -46,6 +46,7 @@ struct RuntimeRules { payload_bound_grants: bool, authority_binding_issued_server_side: bool, verified_human_binding_required_for_protected_routes: bool, + ordinary_user_local_channel_authority: String, persona_binding_claimed: bool, user_channel_body_binding_stage: String, maximum_grant_ttl_ms: u64, @@ -266,6 +267,8 @@ fn validate_registry(registry: &NumberedIpcRegistry) -> Result<(), String> { || !runtime.payload_bound_grants || !runtime.authority_binding_issued_server_side || !runtime.verified_human_binding_required_for_protected_routes + || runtime.ordinary_user_local_channel_authority + != "EXPLICIT_INITIALIZATION_PLUS_ACCOUNT_SCOPED_SESSION_PLUS_KEYCHAIN_SECRET" || runtime.persona_binding_claimed || runtime.user_channel_body_binding_stage != "SEPARATE_EVIDENCE_LAYER_NOT_YET_CLAIMED" || !runtime.receipt_required @@ -424,7 +427,27 @@ fn enforce_admission(app: &AppHandle, route: &OperationRoute) -> Result { Ok(format!("VERIFIED_HUMAN:{registry_domain}:{human_number}")) } - None => Err("HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED".into()), + None => { + // An ordinary user's local channel has no globally registered Guanghu + // number yet. Its explicit initialization acknowledgement, account- + // scoped session file and Keychain secret still form an authenticated + // human boundary for that local account. Without this branch every + // protected in-app feature (including the channel Agent) is rendered + // but unusable immediately after the advertised local initialization. + let session = crate::code_repo_login::current_login_session(app)?; + match session { + Some(session) + if session.domain == "PERSONAL_CHANNEL" && session.host == "local.hololake" => + { + let identity = crate::personal_channel::identity_for_channel_runtime(app)?; + Ok(format!( + "AUTHENTICATED_LOCAL_HUMAN:PERSONAL_CHANNEL:{}", + identity.human_subject_id + )) + } + _ => Err("HOLOLAKE_NUMBERED_IPC_VERIFIED_HUMAN_REQUIRED".into()), + } + } } } 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 b282f1946..21104f155 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 @@ -31,6 +31,21 @@ fn json(value: T) -> Result { .map_err(|error| format!("HOLOLAKE_NUMBERED_IPC_RESULT_INVALID: {error}")) } +fn authenticated_human_subject(app: &AppHandle) -> Result, String> { + let state = app.state::(); + if let Some(subject) = crate::zero_point::verified_user_route(&state)? { + return Ok(Some(subject)); + } + let Some(session) = crate::code_repo_login::current_login_session(app)? else { + return Ok(None); + }; + if session.domain != "PERSONAL_CHANNEL" || session.host != "local.hololake" { + return Ok(None); + } + let identity = crate::personal_channel::identity_for_channel_runtime(app)?; + Ok(Some((identity.human_subject_id, session.domain))) +} + pub(crate) async fn dispatch( app: AppHandle, handler: &str, @@ -47,8 +62,7 @@ pub(crate) async fn dispatch( json(crate::human_authorization::get_center(&app)?) } "human_authorization::decide_authorization_request" => { - let state = app.state::(); - let (human_number, _) = crate::zero_point::verified_user_route(&state)? + let (human_number, _) = authenticated_human_subject(&app)? .ok_or_else(|| "HOLOLAKE_AUTHORIZATION_VERIFIED_HUMAN_REQUIRED".to_string())?; json(crate::human_authorization::decide( &app, @@ -57,14 +71,12 @@ pub(crate) async fn dispatch( )?) } "external_ai_gateway::get_gateway_status" => { - let state = app.state::(); - crate::zero_point::verified_user_route(&state)? + authenticated_human_subject(&app)? .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)? + let (human_number, _) = authenticated_human_subject(&app)? .ok_or_else(|| "HOLOLAKE_GATEWAY_VERIFIED_HUMAN_REQUIRED".to_string())?; json( crate::external_ai_gateway::set_gateway_exposure( @@ -104,8 +116,7 @@ pub(crate) async fn dispatch( json(crate::hldp_tool_forge::compile_tool(app, input(&payload)?)?) } "hldp_tool_forge::request_test_authorization" => { - let state = app.state::(); - let (human_number, _) = crate::zero_point::verified_user_route(&state)? + let (human_number, _) = authenticated_human_subject(&app)? .ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?; json(crate::hldp_tool_forge::request_test_authorization( app, @@ -114,8 +125,7 @@ pub(crate) async fn dispatch( )?) } "hldp_tool_forge::test_tool" => { - let state = app.state::(); - let (human_number, _) = crate::zero_point::verified_user_route(&state)? + let (human_number, _) = authenticated_human_subject(&app)? .ok_or_else(|| "HOLOLAKE_TOOL_FORGE_VERIFIED_HUMAN_REQUIRED".to_string())?; json(crate::hldp_tool_forge::test_tool(app, input(&payload)?, &human_number).await?) } diff --git a/product-source/hololake-native-desktop/src/main.tsx b/product-source/hololake-native-desktop/src/main.tsx index 0f39dfc17..005ca0af2 100644 --- a/product-source/hololake-native-desktop/src/main.tsx +++ b/product-source/hololake-native-desktop/src/main.tsx @@ -1315,6 +1315,15 @@ function HoloLakeApp() { } catch (error) { setEducationMessage(humanError(error, 'system')) } finally { setEducationBusy(false) } } + useEffect(() => { + if (repoLogin) { + void refreshCompositionModule() + void refreshEducationModule() + } else { + setCompositionModule(null) + setEducationModule(null) + } + }, [repoLogin?.username, repoLogin?.domain]) const refreshWebNovelModule = async () => { try { @@ -1469,6 +1478,8 @@ function HoloLakeApp() { humanConfirmedPermissionExpansion: item.artifactKind === 'PHYSICAL_MODULE' && item.permissions.length > 0, } }) setMarketplace(result.snapshot) + if (item.itemNumber === 'HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001') await refreshCompositionModule() + if (item.itemNumber === 'HLP-MOD-OFFICIAL-EDUCATION-WORKBENCH-0001') await refreshEducationModule() setMarketplaceMessage(`${item.displayName} 已完成下载、验签、自检和安装。`) } catch (error) { setMarketplaceMessage(humanError(error, 'system')) @@ -2142,6 +2153,15 @@ function HoloLakeApp() { const climateClasses = worldClimate ? ` climate-${worldClimate.weatherKind.toLowerCase()} phase-${worldClimate.timePhase.toLowerCase()}` : '' const chooseSurface = (next: 'world' | 'traditional') => { setSurface(next) + // The two families have different navigation grammars. Inner world/tool + // stages are not traditional pages, so retaining one while highlighting + // the other family makes the switch look active without changing the UI. + // Enter the selected family's real home and let the user continue there. + if (repoLogin) { + setWorldStage('domain') + setView('overview') + setPublicDomain(null) + } window.localStorage.setItem('hololake-surface', next) } const setFinish = (next: FinishId) => { @@ -2309,6 +2329,20 @@ function HoloLakeApp() { openWorldTool('knowledge')}/> openWorldTool('knowledge')}/> openWorldTool('knowledge')}/> + {compositionModule?.installedState === 'ACTIVE' && ( + { openWorldTool('composition'); void refreshCompositionModule() }}/> + )} + {educationModule?.installedState === 'ACTIVE' && ( + + )} openWorldTool('system')}/> : <> diff --git a/product-source/hololake-native-desktop/src/modules/knowledge-agent/index.tsx b/product-source/hololake-native-desktop/src/modules/knowledge-agent/index.tsx index 64008c618..92fe793ea 100644 --- a/product-source/hololake-native-desktop/src/modules/knowledge-agent/index.tsx +++ b/product-source/hololake-native-desktop/src/modules/knowledge-agent/index.tsx @@ -97,6 +97,7 @@ function kernelStateLabel(runtime: RuntimeSnapshot | null) { export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowledgePath?: string; onClose: () => void }) { const [runtime, setRuntime] = useState(null) + const [runtimeLoading, setRuntimeLoading] = useState(true) const [conversation, setConversation] = useState(null) const [conversationList, setConversationList] = useState([]) const [deleteTarget, setDeleteTarget] = useState(null) @@ -115,15 +116,21 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled const scrollRef = useRef(null) const refresh = async (preferredConversationId?: string) => { - const [nextRuntime, nextList] = await Promise.all([ - invoke('get_persona_agent_runtime'), - invoke('list_persona_agent_conversations'), - ]) - const conversationId = preferredConversationId || conversation?.conversationId || nextList.activeConversationId - const nextConversation = await invoke('get_persona_agent_conversation_by_id', { input: { conversationId } }) - setRuntime(nextRuntime); setConversationList(nextList.conversations); setConversation(nextConversation) - const available = nextRuntime.providers.find((provider) => provider.state === 'AVAILABLE') || nextRuntime.providers[0] - if (available) { setProviderId(available.providerId); setModel(available.selectedModel || available.models[0] || 'qwen3.8-max') } + setRuntimeLoading(true) + try { + // Publish the runtime as soon as it is verified. Conversation hydration is + // independent and must not turn a valid kernel snapshot into a fake zero. + const nextRuntime = await invoke('get_persona_agent_runtime') + setRuntime(nextRuntime) + const available = nextRuntime.providers.find((provider) => provider.state === 'AVAILABLE') || nextRuntime.providers[0] + if (available) { setProviderId(available.providerId); setModel(available.selectedModel || available.models[0] || 'qwen3.8-max') } + const nextList = await invoke('list_persona_agent_conversations') + setConversationList(nextList.conversations) + const conversationId = preferredConversationId || conversation?.conversationId || nextList.activeConversationId + setConversation(await invoke('get_persona_agent_conversation_by_id', { input: { conversationId } })) + } finally { + setRuntimeLoading(false) + } } useEffect(() => { void refresh().catch((reason) => setError(friendlyError(reason))) }, []) @@ -179,7 +186,7 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled const send = async () => { const content = draft.trim() if (!content || sending) return - const optimistic: Message = { messageId: `optimistic-${crypto.randomUUID()}`, role: 'human', participantNumber: runtime?.humanNumber || 'ICE-GL∞', participantName: runtime?.humanName || '冰朔', content, providerId, model, createdAtUnixMs: Date.now(), stateVersion: (conversation?.stateVersion || 0) + 1, receiptHash: '写入中', toolReceipts: [] } + const optimistic: Message = { messageId: `optimistic-${crypto.randomUUID()}`, role: 'human', participantNumber: runtime?.humanNumber || 'HLP-HUMAN-PENDING', participantName: runtime?.humanName || '当前人类', content, providerId, model, createdAtUnixMs: Date.now(), stateVersion: (conversation?.stateVersion || 0) + 1, receiptHash: '写入中', toolReceipts: [] } setOptimisticMessages([optimistic]); setDraft(''); setSending(true); setError(''); setProgress([]) try { const next = await invoke('send_persona_agent_message', { input: { conversationId: conversation?.conversationId || null, providerId, model, content, activeKnowledgePath: activeKnowledgePath || null } }) @@ -191,10 +198,10 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled } return