feat(domain): project verified runtime policy to desktop

This commit is contained in:
冰朔 2026-08-10 10:25:25 +08:00
commit 683e135f5e
12 changed files with 224 additions and 25 deletions

View file

@ -294,6 +294,11 @@ test('current registries, signed node claim and verified handoff project runtime
assert.equal(status.stage, 'runtime-ready');
assert.equal(status.runtimeReady, true);
assert.deepEqual(status.blockers, []);
assert.equal(status.runtimePolicy?.themeOwner, 'fifth-domain');
assert.equal(status.runtimePolicy?.themePackageRef, 'theme://origin-domain/lake-reflects-stars');
assert.equal(status.runtimePolicy?.routeRef, 'domain-route://origin-domain/runtime');
assert.deepEqual(status.runtimePolicy?.allowedSessionScopes, ['domain:enter', 'knowledge:read']);
assert.doesNotMatch(JSON.stringify(status.runtimePolicy), /signature|signer|repository|publicKey/i);
});
test('source failures are reduced to safe status without raw details', async () => {

View file

@ -4,6 +4,7 @@ import {
type DomainAccessEvidence,
type DomainNodeType,
type DomainAccessStatus,
type DomainRuntimePolicyProjection,
} from './domain-access.js';
import {
TrustedSignerSnapshotLoader,
@ -57,6 +58,7 @@ export interface DomainAccessProjection {
nodeId: string;
nodeType: DomainNodeType;
nodeRegistrationSource: Readonly<NodeRegistrationSnapshotReceipt>;
runtimePolicy?: DomainRuntimePolicyProjection;
runtimeReady: boolean;
stage: DomainAccessStatus['stage'];
trustSource: Readonly<TrustedSignerSnapshotReceipt>;

View file

@ -208,6 +208,27 @@ test('runtime access requires matching manifest, scoped capability and online re
assert.equal(status.runtimeReady, true);
assert.equal(status.stage, 'runtime-ready');
assert.deepEqual(status.blockers, []);
assert.deepEqual(status.runtimePolicy, {
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
forbiddenDataScopes: ['private:relationship-core'],
manifestDigest: DIGEST,
permissionPolicyRef: 'policy://origin-domain/default',
routeRef: 'domain-route://origin-domain/runtime',
themeOwner: 'fifth-domain',
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
});
assert.equal(Object.isFrozen(status.runtimePolicy), true);
assert.equal(Object.isFrozen(status.runtimePolicy?.allowedSessionScopes), true);
assert.deepEqual(Object.keys(status.runtimePolicy ?? {}).sort(), [
'allowedSessionScopes',
'forbiddenDataScopes',
'manifestDigest',
'permissionPolicyRef',
'routeRef',
'themeOwner',
'themePackageRef',
]);
assert.doesNotMatch(JSON.stringify(status.runtimePolicy), /signature|signer|repository|sourceCommit|publicKey/i);
});
test('the runtime handoff entry resolves its signer only from a loaded snapshot', async () => {
@ -302,6 +323,7 @@ test('mismatched or expired evidence fails closed', () => {
sessionCapability: { capabilityId: 'CAP-001', domainId: 'DOM-FIFTH-0001', expiresAt: NOW, nodeId: 'LOCAL-001', nodeType: NODE_TYPE, scopes: ['domain:enter'] },
}, NOW);
assert.equal(status.runtimeReady, false);
assert.equal(status.runtimePolicy, undefined);
assert.ok(status.blockers.includes('scoped_session_capability_missing'));
assert.ok(status.blockers.includes('matching_connection_receipt_missing'));
});
@ -446,5 +468,6 @@ test('copying verified-looking fields cannot bypass the in-process verifier boun
sessionCapability: verifiedHandoff.sessionCapability,
}, NOW);
assert.equal(status.runtimeReady, false);
assert.equal(status.runtimePolicy, undefined);
assert.ok(status.blockers.includes('verified_domain_manifest_missing'));
});

View file

@ -99,12 +99,23 @@ export interface DomainAccessEvidence {
sessionCapability?: DomainSessionCapability;
}
export interface DomainRuntimePolicyProjection {
allowedSessionScopes: readonly string[];
forbiddenDataScopes: readonly string[];
manifestDigest: string;
permissionPolicyRef: string;
routeRef: string;
themeOwner: DomainThemeOwner;
themePackageRef: string;
}
export interface DomainAccessStatus {
blockers: string[];
domainId: string;
localWorkspaceAllowed: true;
nodeId: string;
nodeType: DomainNodeType;
runtimePolicy?: DomainRuntimePolicyProjection;
runtimeReady: boolean;
stage: 'login-required' | 'identity-verified' | 'runtime-ready';
}
@ -347,12 +358,24 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
blockers.push('matching_connection_receipt_missing');
}
const runtimeReady = blockers.length === 0;
const runtimePolicy = runtimeReady && manifest
? Object.freeze({
allowedSessionScopes: Object.freeze([...manifest.runtimePolicy.allowedSessionScopes]),
forbiddenDataScopes: Object.freeze([...manifest.runtimePolicy.forbiddenDataScopes]),
manifestDigest: manifest.digest,
permissionPolicyRef: manifest.runtimePolicy.permissionPolicyRef,
routeRef: manifest.runtimePolicy.routeRef,
themeOwner: manifest.runtimePolicy.themeOwner,
themePackageRef: manifest.runtimePolicy.themePackageRef,
})
: undefined;
return {
blockers,
domainId: evidence.domainId,
localWorkspaceAllowed: true,
nodeId: evidence.nodeId,
nodeType: evidence.nodeType,
...(runtimePolicy ? { runtimePolicy } : {}),
runtimeReady,
stage: runtimeReady
? 'runtime-ready'

View file

@ -13,6 +13,7 @@ import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
import { cleanDisplayText } from './presentation';
import { WorldEntry } from './components/WorldEntry';
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
import type { DomainAccessProjection } from './domain-connection';
import { createDomainEntryTarget, type DomainEntryTarget, type DomainNodeType } from './domain-entry-state';
import type { DomainRouteId } from './public-domain-directory';
@ -43,22 +44,6 @@ interface ServerProfile {
channelSubtitle?: string;
}
interface DomainAccessStatus {
blockers: string[];
domainId?: string;
nodeId?: string;
nodeType?: DomainNodeType;
runtimeReady: boolean;
stage: 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready';
trustSource?: {
reason?: 'ANCHOR_INVALID' | 'REGISTRY_INVALID' | 'SOURCE_UNAVAILABLE';
registryVersion: string | null;
signerCount: number;
sourceCommit: string | null;
status: 'CURRENT' | 'DEGRADED_LAST_KNOWN_GOOD' | 'UNAVAILABLE';
};
}
function findFirstDocument(nodes: DocTreeNode[]): string | null {
for (const node of nodes) {
if (node.type === 'document') return node.path;
@ -103,7 +88,7 @@ export default function App() {
const [moduleMessage, setModuleMessage] = useState('');
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
const [worldEntered, setWorldEntered] = useState(false);
const [domainAccess, setDomainAccess] = useState<DomainAccessStatus>({ blockers: [], runtimeReady: false, stage: 'checking' });
const [domainAccess, setDomainAccess] = useState<DomainAccessProjection>({ blockers: [], runtimeReady: false, stage: 'checking' });
const domainAccessRequest = useRef(0);
const storageMode = repositoryStatus?.remote ? 'server' : 'local';

View file

@ -1,5 +1,5 @@
import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react';
import { projectDomainConnectionSteps, projectDomainTrustSource, type DomainAccessProjection } from '../domain-connection';
import { projectDomainConnectionSteps, projectDomainRuntimePolicyReceipt, projectDomainTrustSource, type DomainAccessProjection } from '../domain-connection';
import { canEnterSelectedDomainRuntime, projectDomainRuntimeBoundary, type DomainEntryTarget, type DomainNodeType } from '../domain-entry-state';
import { publicDomainDirectory, type DomainRouteId } from '../public-domain-directory';
@ -17,6 +17,7 @@ interface Props {
export function DomainConnectionSheet({ access, target, open, onClose, onSelectTarget, onSelectNodeType, onEnterRuntime, onOpenCodeChannel }: Props) {
if (!open) return null;
const steps = projectDomainConnectionSteps(access);
const policyReceipt = projectDomainRuntimePolicyReceipt(access);
const canEnter = canEnterSelectedDomainRuntime(target, access);
return (
@ -54,6 +55,13 @@ export function DomainConnectionSheet({ access, target, open, onClose, onSelectT
))}
</ol>
{policyReceipt.length > 0 && <section className="domain-policy-receipt" aria-labelledby="domain-policy-receipt-title">
<h3 id="domain-policy-receipt-title"></h3>
<dl>
{policyReceipt.map(row => <div key={row.label}><dt>{row.label}</dt><dd>{row.value}</dd></div>)}
</dl>
</section>}
<p className="domain-connection-boundary">{projectDomainRuntimeBoundary(target, access)} </p>
</>}

View file

@ -1,6 +1,16 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { projectDomainConnectionSteps, projectDomainTrustSource } from './domain-connection.js';
import { projectDomainConnectionSteps, projectDomainRuntimePolicyReceipt, projectDomainTrustSource } from './domain-connection.js';
const verifiedRuntimePolicy = {
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
forbiddenDataScopes: ['private:relationship-core'],
manifestDigest: 'a'.repeat(64),
permissionPolicyRef: 'policy://origin-domain/default',
routeRef: 'domain-route://origin-domain/runtime',
themeOwner: 'fifth-domain' as const,
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
};
test('code-channel account absence cannot be projected as a domain runtime login', () => {
const steps = projectDomainConnectionSteps({
@ -32,10 +42,29 @@ test('verified account and node remain identity-only without domain handoff evid
});
test('runtime-ready requires all four verified steps', () => {
const steps = projectDomainConnectionSteps({ blockers: [], runtimeReady: true, stage: 'runtime-ready' });
const steps = projectDomainConnectionSteps({ blockers: [], runtimePolicy: verifiedRuntimePolicy, runtimeReady: true, stage: 'runtime-ready' });
assert.ok(steps.every(step => step.state === 'verified'));
});
test('a runtime-ready access projects only a compact verified policy receipt', () => {
const rows = projectDomainRuntimePolicyReceipt({
blockers: [],
runtimePolicy: verifiedRuntimePolicy,
runtimeReady: true,
stage: 'runtime-ready',
});
assert.deepEqual(rows.map(row => row.label), ['主题包', '运行路线', '权限策略', '允许会话', '禁止数据', '清单摘要']);
assert.match(rows[0].value, /^theme:\/\/origin-domain\//);
assert.match(rows.at(-1)?.value ?? '', /^a{12}…a{8}$/);
assert.doesNotMatch(JSON.stringify(rows), /signature|signer|repository|publicKey/i);
assert.deepEqual(projectDomainRuntimePolicyReceipt({
blockers: ['matching_connection_receipt_missing'],
runtimePolicy: verifiedRuntimePolicy,
runtimeReady: false,
stage: 'identity-verified',
}), []);
});
test('a current empty signer registry is projected as a real manifest blocker', () => {
const access = {
blockers: ['verified_domain_manifest_missing'],

View file

@ -1,10 +1,21 @@
export type DomainAccessStage = 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready';
export interface VerifiedDomainRuntimePolicyProjection {
allowedSessionScopes: readonly string[];
forbiddenDataScopes: readonly string[];
manifestDigest: string;
permissionPolicyRef: string;
routeRef: string;
themeOwner: 'domain-team' | 'fifth-domain';
themePackageRef: string;
}
export interface DomainAccessProjection {
blockers: string[];
domainId?: string;
nodeId?: string;
nodeType?: 'local-terminal' | 'cloud-resident';
runtimePolicy?: VerifiedDomainRuntimePolicyProjection;
runtimeReady: boolean;
stage: DomainAccessStage;
trustSource?: {
@ -23,6 +34,11 @@ export interface DomainConnectionStep {
detail: string;
}
export interface DomainRuntimePolicyReceiptRow {
label: string;
value: string;
}
const blockerToStep: Record<string, DomainConnectionStep['id']> = {
account_identity_missing: 'identity',
verified_node_registration_missing: 'identity',
@ -90,3 +106,16 @@ export function projectDomainTrustSource(access: DomainAccessProjection): string
const health = source.status === 'CURRENT' ? '当前' : '上次可信快照';
return `可信清单 ${health} · v${version} · ${commit} · 已登记签名人 ${source.signerCount}`;
}
export function projectDomainRuntimePolicyReceipt(access: DomainAccessProjection): DomainRuntimePolicyReceiptRow[] {
const policy = access.runtimeReady ? access.runtimePolicy : undefined;
if (!policy) return [];
return [
{ label: '主题包', value: policy.themePackageRef },
{ label: '运行路线', value: policy.routeRef },
{ label: '权限策略', value: policy.permissionPolicyRef },
{ label: '允许会话', value: policy.allowedSessionScopes.join(' · ') },
{ label: '禁止数据', value: policy.forbiddenDataScopes.join(' · ') },
{ label: '清单摘要', value: `${policy.manifestDigest.slice(0, 12)}${policy.manifestDigest.slice(-8)}` },
];
}

View file

@ -2,7 +2,24 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { canEnterSelectedDomainRuntime, createDomainEntryTarget, projectDomainRuntimeBoundary } from './domain-entry-state.js';
const ready = (domainId: string, nodeType: 'local-terminal' | 'cloud-resident' = 'local-terminal') => ({ blockers: [], domainId, nodeType, runtimeReady: true, stage: 'runtime-ready' as const });
const validRuntimePolicy = {
allowedSessionScopes: ['domain:enter', 'knowledge:read'],
forbiddenDataScopes: ['private:relationship-core'],
manifestDigest: 'a'.repeat(64),
permissionPolicyRef: 'policy://origin-domain/default',
routeRef: 'domain-route://origin-domain/runtime',
themeOwner: 'fifth-domain' as const,
themePackageRef: 'theme://origin-domain/lake-reflects-stars',
};
const ready = (domainId: string, nodeType: 'local-terminal' | 'cloud-resident' = 'local-terminal') => ({
blockers: [],
domainId,
nodeType,
runtimePolicy: validRuntimePolicy,
runtimeReady: true,
stage: 'runtime-ready' as const,
});
test('a generic login request does not silently target the Fifth Domain', () => {
assert.equal(canEnterSelectedDomainRuntime(null, ready('DOM-FIFTH-0001')), false);
@ -30,3 +47,20 @@ test('the Fifth Domain opens only with matching runtime-ready evidence', () => {
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001', 'local-terminal')), /另一种节点类型/);
assert.match(projectDomainRuntimeBoundary(fifth, ready('DOM-FIFTH-0001', 'cloud-resident')), /已经匹配/);
});
test('the Fifth Domain stays closed without the exact verified runtime policy projection', () => {
const fifth = createDomainEntryTarget('fifth');
const base = ready('DOM-FIFTH-0001');
assert.equal(canEnterSelectedDomainRuntime(fifth, { ...base, runtimePolicy: undefined }), false);
assert.match(projectDomainRuntimeBoundary(fifth, { ...base, runtimePolicy: undefined }), /可信运行策略缺失或不匹配/);
for (const runtimePolicy of [
{ ...validRuntimePolicy, manifestDigest: 'not-a-digest' },
{ ...validRuntimePolicy, themeOwner: 'domain-team' as const },
{ ...validRuntimePolicy, themePackageRef: 'theme://enterprise/fifth-look' },
{ ...validRuntimePolicy, routeRef: 'domain-route://enterprise/fifth' },
{ ...validRuntimePolicy, allowedSessionScopes: ['knowledge:read'] },
]) {
assert.equal(canEnterSelectedDomainRuntime(fifth, { ...base, runtimePolicy }), false);
}
});

View file

@ -20,7 +20,14 @@ export function canEnterSelectedDomainRuntime(target: DomainEntryTarget | null,
if (access.nodeType !== target.nodeType) return false;
// The present desktop bundle contains only the Fifth Domain renderer. The four
// enterprise domains must provide their own signed runtime package and endpoint.
return target.domain.routeId === 'fifth';
if (target.domain.routeId !== 'fifth') return false;
const policy = access.runtimePolicy;
return Boolean(policy
&& /^[a-f0-9]{64}$/.test(policy.manifestDigest)
&& policy.themeOwner === 'fifth-domain'
&& policy.themePackageRef.startsWith('theme://origin-domain/')
&& policy.routeRef.startsWith('domain-route://origin-domain/')
&& policy.allowedSessionScopes.includes('domain:enter'));
}
export function projectDomainRuntimeBoundary(target: DomainEntryTarget | null, access: DomainAccessProjection): string {
@ -34,7 +41,8 @@ export function projectDomainRuntimeBoundary(target: DomainEntryTarget | null, a
if (target.domain.routeId !== 'fifth') {
return `${target.domain.displayName}的独立运行端点与主题包尚未登记;当前只能查看公开门厅。`;
}
return access.runtimeReady
? '第五域运行端点、签名清单、会话能力与在线回执已经匹配。'
: '第五域入口保持关闭,直到四项真实接入证据全部匹配。';
if (!access.runtimeReady) return '第五域入口保持关闭,直到四项真实接入证据全部匹配。';
return canEnterSelectedDomainRuntime(target, access)
? '第五域运行端点、签名清单、主题策略、会话能力与在线回执已经匹配。'
: '域接入证据声称已就绪,但可信运行策略缺失或不匹配;第五域入口保持关闭。';
}

View file

@ -3240,6 +3240,27 @@ select:focus-visible {
.domain-connection-steps em { color: #d9ad5f; font-size: 9px; font-style: normal; }
.domain-connection-steps li[data-state="verified"] em { color: var(--lake-success); }
.domain-policy-receipt {
margin: 12px 20px 0;
padding: 13px 14px;
border: 1px solid rgba(81, 200, 120, .2);
border-radius: 10px;
background: rgba(8, 26, 29, .58);
}
.domain-policy-receipt h3 {
margin: 0 0 9px;
color: var(--lake-success);
font-size: 10px;
font-weight: 600;
letter-spacing: .08em;
}
.domain-policy-receipt dl { display: grid; gap: 6px; margin: 0; }
.domain-policy-receipt dl > div { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 10px; }
.domain-policy-receipt dt { color: var(--lake-muted); font-size: 9px; }
.domain-policy-receipt dd { margin: 0; overflow-wrap: anywhere; color: var(--lake-text-soft); font-size: 9px; line-height: 1.45; }
.domain-connection-boundary {
margin-bottom: 18px;
padding: 10px 12px;

View file

@ -0,0 +1,32 @@
# HoloLake 桌面端可信域策略投影验收
日期2026-08-10
阶段DEV-20260809-007 / Stage 008
状态:实现前验收基线
## 目标
把服务端已经完成密码学核验的域运行策略,以只读、最小化的方式投影到真实 HoloLake 桌面入口。桌面端在载入第五域运行体前,除了核对域编号和节点类型,还必须核对签名清单绑定的运行路线、主题归属、主题包和会话权限。
这不是新增一套前端 manifest也不是让 UI 自己判断签名唯一权威仍是桌面进程内已经通过可信签名人快照、Ed25519 签名、清单摘要、会话能力和在线回执共同验证的服务端结果。
## 验收标准
1. 只有本进程内完成密码学核验、且四项域接入证据全部匹配时,服务端才返回 `runtimePolicy` 投影。
2. 投影仅包含manifest 摘要、运行路线、主题归属、主题包、权限策略、允许的会话范围和禁止的数据范围。
3. 投影不得包含签名、签名人公钥、仓库地址、源提交、连接凭据或任何私人数据。
4. 非 `runtime-ready` 状态不得携带策略投影;复制一份外观相同的 manifest 也不能越过进程内验证边界。
5. 第五域最终入口必须同时满足:稳定域编号和节点类型匹配、摘要格式有效、主题归属为 `fifth-domain`、主题包属于 `theme://origin-domain/`、运行路线属于 `domain-route://origin-domain/`,并允许 `domain:enter`
6. 缺少或不匹配可信策略投影时,即使上层错误声称 `runtimeReady`,第五域渲染器也保持关闭,并向用户说明是策略回读不完整或不匹配。
7. 四个企业域继续只有公开门厅;在各自独立运行端点和主题包真正登记、打包以前,不得复用第五域渲染器。
8. 域接入面板以紧凑、可读的可信回执展示真实策略,不制造假状态,不暴露验证内部秘密。
9. 公共灯塔和五域公开门厅的匿名可见边界不变;本阶段不改变已选定的公共主题或第五域主题。
10. 本阶段只证明代码、测试、构建、安装和运行中实际完成的层级;真实生产 manifest、注册端点、handoff 端点或生产会话不存在时继续明确记为 `NOT_EXISTS_0`
## 失败关闭边界
- 签名清单未验证:不投影策略,不进入域。
- 策略投影缺失或被篡改:不进入域。
- 域、节点、路线、主题归属、主题命名空间或权限任一不匹配:不进入域。
- 代码频道登录、仓库同步或本地知识工作区可用:均不得被解释为已经取得域运行权限。