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