fix(persona): gate runtime projection by repository binding

This commit is contained in:
冰朔 2026-08-12 02:42:17 +08:00
commit ebe12763f1
6 changed files with 111 additions and 15 deletions

View file

@ -109,6 +109,9 @@ REVISE | REFUSE`。
- 人格语言唤醒绑定编译器源码为 `100`:只接受唯一且干净的人格根、已登记本机设备身份、与 manifest
精确一致的模型端点及唯一可激活只读事实器官,并外显冰朔责任、铸渊认知作者和本轮开发归因。
它不创建人格仓库、不注册节点,也不把源码编译结果冒充桌面运行集成。
- 人格运行投影的仓库前置门源码为 `100`:它必须先得到唯一证据绑定,才允许向原生只读运行查询传入
该一个规范仓库;未绑定和歧义状态直接保持原义,不再把普通挂载仓库的 manifest 缺失显示为人格运行时
回执故障。该门只收束事实读取范围,不创建、选择或唤醒人格仓库。
- 完整 HoloLake Runtime 与单 AGE 纵向闭环仍为 `0`:桌面语言入口绑定、真实人格仓库 manifest 绑定和
桌面运行验收尚未完成,因此不能用本轮源码测试冒充可用产品。
- Mirror runner、制品、部署和运行健康`0`

View file

@ -1262,9 +1262,9 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins
- **AI feature visibility**`ai_features_visibility_changed` records only whether installation-level AI surfaces were enabled or hidden.
- **Automatic update checks**`automatic_update_checks_changed` records only whether startup/background update checks were enabled or disabled.
- **All Notes visibility**`all_notes_visibility_changed` records only the toggled category and enabled state.
- **Persona runtime projection**`persona_runtime_projection_loaded` and `persona_runtime_projection_retry`
record only coarse phase and count fields. Repository paths, persona session ids, event hashes, and runtime
attribution are never sent.
- **Persona runtime projection**`persona_runtime_projection_gated`, `persona_runtime_projection_loaded`,
and `persona_runtime_projection_retry` record only coarse phase and count fields. Repository paths, persona
session ids, event hashes, and runtime attribution are never sent.
### Tauri Commands
- **`reinit_telemetry`** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting.

View file

@ -125,9 +125,11 @@ shows the exact repository, Git head, checkpoint, pinned model, and B0 evidence.
sovereign repository, register a node, acquire a lease, wake an organ, or run inference. Unbound is never
presented as proof that the persona does not exist, and multiple valid roots are not selected automatically.
The runtime projection independently queries each mounted path for `ICE-P-ZY001` through the native read
model. The renderer validates the receipt identity and required evidence, preserves partial query errors,
and displays the newest verified
The runtime projection repeats the fail-closed discovery boundary and queries the native read model only
after exactly one persona repository is evidence-bound. It passes only that canonical repository path into
the runtime query; unavailable, unbound, and ambiguous discovery states never query ordinary mounted
repositories and never become false runtime-receipt failures. The renderer validates the receipt identity
and required evidence, preserves query errors from the bound repository, and displays the newest verified
session with both a human state label and the raw state, Git and event-chain heads, node, organ, timestamp,
and human responsibility subject. No matching session is reported as absence of matching evidence, never as
proof that the persona is offline. This surface contains no wake, inference, lease, or execution control.

View file

@ -2,19 +2,29 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PersonaRuntimeProjectionPanel } from './PersonaRuntimeProjectionPanel'
const { loadPersonaRuntimeProjectionMock, trackEventMock } = vi.hoisted(() => ({
const { loadPersonaRuntimeProjectionMock, resolvePersonaRepositoryBindingMock, trackEventMock } = vi.hoisted(() => ({
loadPersonaRuntimeProjectionMock: vi.fn(),
resolvePersonaRepositoryBindingMock: vi.fn(),
trackEventMock: vi.fn(),
}))
vi.mock('../lib/personaRuntimeProjection', () => ({
loadPersonaRuntimeProjection: loadPersonaRuntimeProjectionMock,
}))
vi.mock('../lib/personaRepositoryBinding', () => ({
resolvePersonaRepositoryBinding: resolvePersonaRepositoryBindingMock,
}))
vi.mock('../lib/telemetry', () => ({ trackEvent: trackEventMock }))
describe('PersonaRuntimeProjectionPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
resolvePersonaRepositoryBindingMock.mockResolvedValue({
phase: 'bound',
inspectedRepositoryCount: 1,
failures: [],
binding: { repositoryPath: '/repo/persona' },
})
loadPersonaRuntimeProjectionMock.mockResolvedValue({
errors: [],
inspectedRepositoryCount: 1,
@ -69,6 +79,10 @@ describe('PersonaRuntimeProjectionPanel', () => {
expect(screen.getByText('event-chain-verified')).toBeInTheDocument()
expect(screen.getByText(/guanghu\.zhuyuan-cognitive-gravity-frame\/v1/)).toBeInTheDocument()
expect(screen.getByText('1111111111111111111111111111111111111111')).toBeInTheDocument()
expect(loadPersonaRuntimeProjectionMock).toHaveBeenCalledWith({
personaId: 'ICE-P-ZY001',
repositoryPaths: ['/repo/persona'],
})
expect(trackEventMock).toHaveBeenCalledWith('persona_runtime_projection_loaded', {
error_count: 0,
inspected_repository_count: 1,
@ -125,4 +139,43 @@ describe('PersonaRuntimeProjectionPanel', () => {
await waitFor(() => expect(loadPersonaRuntimeProjectionMock).toHaveBeenCalledTimes(2))
expect(trackEventMock).toHaveBeenCalledWith('persona_runtime_projection_retry')
})
it('does not query runtime receipts when no persona repository is bound', async () => {
resolvePersonaRepositoryBindingMock.mockResolvedValue({
phase: 'unbound',
inspectedRepositoryCount: 1,
failures: [{ repositoryPath: '/ordinary', code: 'PERSONA_MANIFEST_READ_FAILED' }],
})
render(
<PersonaRuntimeProjectionPanel
locale="zh-CN"
personaId="ICE-P-ZY001"
repositoryPaths={['/ordinary']}
/>,
)
await waitFor(() => expect(screen.getByText('尚未绑定人格代码仓库')).toBeInTheDocument())
expect(screen.getByText('这只说明当前挂载范围没有匹配的完整证据,不能据此判定人格不存在。')).toBeInTheDocument()
expect(loadPersonaRuntimeProjectionMock).not.toHaveBeenCalled()
})
it('refuses runtime projection when repository discovery is ambiguous', async () => {
resolvePersonaRepositoryBindingMock.mockResolvedValue({
phase: 'ambiguous',
inspectedRepositoryCount: 2,
failures: [],
})
render(
<PersonaRuntimeProjectionPanel
locale="zh-CN"
personaId="ICE-P-ZY001"
repositoryPaths={['/persona-a', '/persona-b']}
/>,
)
await waitFor(() => expect(screen.getByText('发现多个有效仓库,拒绝自动选择')).toBeInTheDocument())
expect(loadPersonaRuntimeProjectionMock).not.toHaveBeenCalled()
})
})

View file

@ -5,6 +5,10 @@ import {
type PersonaRuntimeProjection,
type PersonaRuntimeSessionProjection,
} from '../lib/personaRuntimeProjection'
import {
resolvePersonaRepositoryBinding,
type PersonaRepositoryBindingResolution,
} from '../lib/personaRepositoryBinding'
import { trackEvent } from '../lib/telemetry'
import { Button } from './ui/button'
@ -14,7 +18,8 @@ type PersonaRuntimeProjectionPanelProps = {
repositoryPaths: readonly string[]
}
type PanelState = PersonaRuntimeProjection | { phase: 'checking' }
type RepositoryGateState = Exclude<PersonaRepositoryBindingResolution, { phase: 'bound' }>
type PanelState = PersonaRuntimeProjection | RepositoryGateState | { phase: 'checking' }
function stateTranslationKey(state: string): TranslationKey {
if (state.startsWith('DORMANT')) return 'hololake.personaRuntime.state.dormant'
@ -100,10 +105,25 @@ export function PersonaRuntimeProjectionPanel({
useEffect(() => {
let active = true
void loadPersonaRuntimeProjection({
personaId,
repositoryPaths: repositorySignature ? repositorySignature.split('\u0000') : [],
}).then((projection) => {
const load = async () => {
const bindingResolution = await resolvePersonaRepositoryBinding({
personaId,
repositoryPaths: repositorySignature ? repositorySignature.split('\u0000') : [],
})
if (!active) return
if (bindingResolution.phase !== 'bound') {
setState(bindingResolution)
trackEvent('persona_runtime_projection_gated', {
failure_count: bindingResolution.failures.length,
inspected_repository_count: bindingResolution.inspectedRepositoryCount,
phase: bindingResolution.phase,
})
return
}
const projection = await loadPersonaRuntimeProjection({
personaId,
repositoryPaths: [bindingResolution.binding.repositoryPath],
})
if (!active) return
setState(projection)
trackEvent('persona_runtime_projection_loaded', {
@ -112,7 +132,8 @@ export function PersonaRuntimeProjectionPanel({
phase: projection.phase,
session_count: projection.sessions.length,
})
})
}
void load()
return () => {
active = false
}
@ -143,10 +164,26 @@ export function PersonaRuntimeProjectionPanel({
</div>
) : null}
{state.phase === 'unbound' ? (
<div className="persona-runtime-projection__message">
<strong>{translate(locale, 'hololake.personaRepository.unbound')}</strong>
<p>{translate(locale, 'hololake.personaRepository.unboundDescription')}</p>
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
</div>
) : null}
{state.phase === 'ambiguous' ? (
<div className="persona-runtime-projection__message persona-runtime-projection__message--error" role="alert">
<strong>{translate(locale, 'hololake.personaRepository.ambiguous')}</strong>
<p>{translate(locale, 'hololake.personaRepository.ambiguousDescription')}</p>
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
</div>
) : null}
{state.phase === 'error' ? (
<div className="persona-runtime-projection__message persona-runtime-projection__message--error" role="alert">
<strong>{translate(locale, 'hololake.personaRuntime.error')}</strong>
<code>{state.errors[0]?.code}</code>
<code>{'code' in state ? state.code : state.errors[0]?.code}</code>
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
</div>
) : null}

View file

@ -1,7 +1,7 @@
{
"schema": "hololake.current-architecture/v1",
"architecture_id": "HLP-CURRENT-ARCH-001",
"version": "2026-08-12.5",
"version": "2026-08-12.6",
"state": "CURRENT_CANONICAL",
"product": {
"formal_name": "光湖语言系统 · 通用人工智能操作平台",
@ -113,6 +113,7 @@
"persona_repository_binding_discovery_source_implemented": true,
"persona_language_wake_binding_compiler_source_implemented": true,
"persona_repository_binding_read_only_ui_source_integrated": true,
"persona_runtime_projection_repository_gate_source_integrated": true,
"real_persona_repository_manifest_bound": false,
"natural_language_partner_adapter_runtime_integrated": false,
"required_natural_language_entry": "PARTNER_DELIBERATION_AND_LANGUAGE_HOME_INTEGRITY_GATE",