Separate domain access from code-channel login

This commit is contained in:
冰朔 2026-08-10 04:38:41 +08:00
commit 6b1bb203df
7 changed files with 324 additions and 6 deletions

View file

@ -0,0 +1,36 @@
{
"schema": "guanghu.desktop-delivery-receipt/v1",
"receipt_id": "GH-HOLOLAKE-DOMAIN-CONNECTION-SEPARATION-20260810-001",
"development_id": "DEV-20260809-007",
"source_base_commit": "066f14d0074bd6315fd21da4fbd39948559ea2c6",
"source_commit": "SELF",
"repository": "repo://guanghulab.com/code/bingshuo/hololake-system-architecture#main",
"acceptance": {
"public_entry_opens_domain_gate": true,
"code_channel_account_is_not_domain_login": true,
"runtime_entry_fails_closed_without_handoff": true,
"local_workspace_remains_explicitly_separate": true,
"local_and_cloud_node_modes_are_truthfully_projected": true
},
"implementation": {
"domain_connection_sheet": "Projects account and registered-node identity, signed domain manifest, scoped session capability and matching online receipt as four separate gates.",
"code_channel_boundary": "Forgejo account and Git remote settings remain available through a separately labelled code-channel and sync path.",
"ui_layering": "Connection and storage dialogs render above the public lighthouse instead of existing only in the accessibility tree behind it.",
"status_copy": "The workspace top bar now says code-channel account and no longer claims that Forgejo authentication is Fifth Domain login."
},
"verification": {
"tests": "PASS 49; FAIL 0",
"production_build": "PASS",
"developer_id_deep_strict_codesign": "PASS",
"installed_app_binary_sha256": "ed4dd0d0dad51a4aed5fac62263710ab502c744aad0271410d9c245118894b60",
"installed_app_asar_sha256": "7dc31460ccfc11042171a03f0c773a0e27cb53c3267c7c9965e36c15943dfcce",
"current_desktop_visual_acceptance": "PASS: public login visibly opened the domain gate; all four unverified gates were shown; runtime entry stayed disabled; code-channel settings opened separately with explicit non-domain-authority copy."
},
"not_claimed": [
"No target-domain signer, live handoff endpoint, domain session capability or online runtime receipt exists yet.",
"No server deployment, production cutover, real Fifth Domain runtime entry or persona-birth claim is made.",
"No Apple notarization was performed; the installed development delivery is Developer ID signed."
],
"result": "PASS_100_LOCAL_DOMAIN_CONNECTION_BOUNDARY",
"verified_at": "2026-08-10T04:37:44+08:00"
}

View file

@ -12,6 +12,7 @@ import { DomainSurface } from './components/DomainSurface';
import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
import { cleanDisplayText } from './presentation';
import { WorldEntry } from './components/WorldEntry';
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
type View = 'editor' | 'history';
type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
@ -80,6 +81,7 @@ export default function App() {
const [activeRoute, setActiveRoute] = useState<RouteId>('fifth');
const [activeModule, setActiveModule] = useState<ModuleId>('knowledge');
const [storageSheetOpen, setStorageSheetOpen] = useState(false);
const [domainConnectionOpen, setDomainConnectionOpen] = useState(false);
const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>();
const [serverSession, setServerSession] = useState<ServerSession>({ authenticated: false, nodeId: '' });
const [serverProfiles, setServerProfiles] = useState<ServerProfile[]>([]);
@ -391,8 +393,7 @@ export default function App() {
setWorldEntered(true);
}}
onOpenConnection={() => {
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
setDomainConnectionOpen(true);
}}
/>
)}
@ -414,7 +415,7 @@ export default function App() {
setStorageSheetOpen(true);
}}>
<span className="session-status-dot" aria-hidden="true" />
<span><strong>{serverSession.authenticated ? serverSession.username : (personalServer ? '登录第五域' : '配置我的服务器')}</strong><small>{personalServer?.id || '本机私有配置'}</small></span>
<span><strong>{serverSession.authenticated ? `代码频道 · ${serverSession.username}` : (personalServer ? '代码频道账号' : '配置我的服务器')}</strong><small>{personalServer?.id || '本机私有配置'}</small></span>
</button>
</header>
@ -577,6 +578,23 @@ export default function App() {
refreshDomainAccess();
}}
/>
<DomainConnectionSheet
access={domainAccess}
open={domainConnectionOpen}
onClose={() => setDomainConnectionOpen(false)}
onEnterRuntime={() => {
if (!domainAccess.runtimeReady) return;
setActiveRoute('fifth');
setActiveModule('knowledge');
setWorldEntered(true);
setDomainConnectionOpen(false);
}}
onOpenCodeChannel={() => {
setDomainConnectionOpen(false);
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}
/>
<HumanSettings
open={humanSettingsOpen}
preferences={humanPreferences}

View file

@ -0,0 +1,60 @@
import { Cloud, Laptop, ShieldCheck, X } from 'lucide-react';
import { projectDomainConnectionSteps, type DomainAccessProjection } from '../domain-connection';
interface Props {
access: DomainAccessProjection;
open: boolean;
onClose: () => void;
onEnterRuntime: () => void;
onOpenCodeChannel: () => void;
}
export function DomainConnectionSheet({ access, open, onClose, onEnterRuntime, onOpenCodeChannel }: Props) {
if (!open) return null;
const steps = projectDomainConnectionSteps(access);
return (
<div className="storage-sheet-backdrop" role="presentation" onMouseDown={event => {
if (event.target === event.currentTarget) onClose();
}}>
<section className="storage-sheet domain-connection-sheet" role="dialog" aria-modal="true" aria-labelledby="domain-connection-title">
<header className="storage-sheet-header">
<div>
<small>DOM-FIFTH-0001</small>
<h2 id="domain-connection-title"> · </h2>
</div>
<button className="icon-button" onClick={onClose} aria-label="关闭域接入状态"><X aria-hidden="true" /></button>
</header>
<p className="domain-connection-lead">线</p>
<div className="domain-node-modes" aria-label="支持的节点类型">
<article><Laptop aria-hidden="true" /><span><strong></strong><small>使</small></span></article>
<article><Cloud aria-hidden="true" /><span><strong></strong><small>线</small></span></article>
</div>
<ol className="domain-connection-steps">
{steps.map((step, index) => (
<li key={step.id} data-state={step.state}>
<span>{step.state === 'verified' ? <ShieldCheck aria-hidden="true" /> : index + 1}</span>
<div><strong>{step.label}</strong><small>{step.detail}</small></div>
<em>{step.state === 'verified' ? '已验证' : step.state === 'checking' ? '核对中' : '待完成'}</em>
</li>
))}
</ol>
<p className="domain-connection-boundary"></p>
<footer className="storage-sheet-footer">
<button className="secondary-button" onClick={onOpenCodeChannel}></button>
<span />
<button className="secondary-button" onClick={onClose}></button>
<button className="primary-button" onClick={onEnterRuntime} disabled={!access.runtimeReady}>
{access.runtimeReady ? '进入第五域运行体' : '等待可信域接入'}
</button>
</footer>
</section>
</div>
);
}

View file

@ -284,7 +284,7 @@ export function StorageLocationSheet({ open, apiBase, currentRemote, initialMode
{!session.authenticated ? (
<div className="server-login-form">
<p className="sheet-label"></p>
<p className="sheet-label"></p>
<div className="login-fields">
<input value={username} onChange={event => setUsername(event.target.value)} placeholder="账号" autoComplete="username" />
<input type="password" value={password} onChange={event => setPassword(event.target.value)} placeholder="密码" autoComplete="current-password" onKeyDown={event => {
@ -292,7 +292,7 @@ export function StorageLocationSheet({ open, apiBase, currentRemote, initialMode
}} />
<button className="secondary-button" onClick={login} disabled={busy || !username || !password}></button>
</div>
<small> HoloLake Git </small>
<small> HoloLake Git </small>
</div>
) : (
<div className="repository-picker">

View file

@ -0,0 +1,37 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { projectDomainConnectionSteps } from './domain-connection.js';
test('code-channel account absence cannot be projected as a domain runtime login', () => {
const steps = projectDomainConnectionSteps({
blockers: [
'account_node_identity_missing',
'verified_domain_manifest_missing',
'scoped_session_capability_missing',
'matching_connection_receipt_missing',
],
runtimeReady: false,
stage: 'login-required',
});
assert.deepEqual(steps.map(step => step.state), ['blocked', 'blocked', 'blocked', 'blocked']);
assert.match(steps[0].detail, /登记的本地终端节点或云常驻节点/);
});
test('verified account and node remain identity-only without domain handoff evidence', () => {
const steps = projectDomainConnectionSteps({
blockers: [
'verified_domain_manifest_missing',
'scoped_session_capability_missing',
'matching_connection_receipt_missing',
],
runtimeReady: false,
stage: 'identity-verified',
});
assert.deepEqual(steps.map(step => step.state), ['verified', 'blocked', 'blocked', 'blocked']);
});
test('runtime-ready requires all four verified steps', () => {
const steps = projectDomainConnectionSteps({ blockers: [], runtimeReady: true, stage: 'runtime-ready' });
assert.ok(steps.every(step => step.state === 'verified'));
});

View file

@ -0,0 +1,62 @@
export type DomainAccessStage = 'checking' | 'login-required' | 'identity-verified' | 'runtime-ready';
export interface DomainAccessProjection {
blockers: string[];
runtimeReady: boolean;
stage: DomainAccessStage;
}
export interface DomainConnectionStep {
id: 'identity' | 'manifest' | 'capability' | 'receipt';
label: string;
state: 'checking' | 'blocked' | 'verified';
detail: string;
}
const blockerToStep: Record<string, DomainConnectionStep['id']> = {
account_node_identity_missing: 'identity',
verified_domain_manifest_missing: 'manifest',
scoped_session_capability_missing: 'capability',
matching_connection_receipt_missing: 'receipt',
desktop_runtime_required: 'identity',
domain_access_probe_failed: 'identity',
};
const stepCopy: Record<DomainConnectionStep['id'], { label: string; pending: string; verified: string }> = {
identity: {
label: '账户与登记节点',
pending: '需要由已登记的本地终端节点或云常驻节点完成身份证明',
verified: '账户声明和节点编号已经验证',
},
manifest: {
label: '目标域签名清单',
pending: '等待目标域可信签名人签发并验证域 manifest',
verified: '目标域 manifest 与可信签名来源一致',
},
capability: {
label: '域会话能力',
pending: '等待签发仅允许进入当前域的短期会话能力',
verified: '当前节点持有有效的 domain:enter 能力',
},
receipt: {
label: '在线连接回执',
pending: '等待目标域运行体返回与节点、清单一致的在线回执',
verified: '连接回执已回读并与当前会话一致',
},
};
export function projectDomainConnectionSteps(access: DomainAccessProjection): DomainConnectionStep[] {
const blockers = new Set(access.blockers.map(blocker => blockerToStep[blocker]).filter(Boolean));
const ordered: DomainConnectionStep['id'][] = ['identity', 'manifest', 'capability', 'receipt'];
return ordered.map(id => {
const copy = stepCopy[id];
const verified = access.runtimeReady || (!blockers.has(id) && access.stage !== 'checking');
return {
id,
label: copy.label,
state: access.stage === 'checking' ? 'checking' : verified ? 'verified' : 'blocked',
detail: access.stage === 'checking' ? '正在核对当前接入证据' : verified ? copy.verified : copy.pending,
};
});
}

View file

@ -2903,7 +2903,7 @@ select:focus-visible {
.storage-sheet-backdrop {
position: fixed;
z-index: 100;
z-index: 1200;
inset: 0;
display: grid;
place-items: center;
@ -3123,6 +3123,111 @@ select:focus-visible {
flex: 1;
}
.domain-connection-sheet .storage-sheet-header > div {
display: grid;
gap: 4px;
}
.domain-connection-sheet .storage-sheet-header small {
color: var(--lake-accent);
font-size: 9px;
letter-spacing: .12em;
}
.domain-connection-lead,
.domain-connection-boundary {
margin: 18px 20px 0;
color: var(--lake-text-soft);
font-size: 12px;
line-height: 1.65;
}
.domain-node-modes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 16px 20px;
}
.domain-node-modes article {
display: flex;
gap: 11px;
padding: 13px;
border: 1px solid var(--lake-border-soft);
border-radius: 11px;
background: rgba(7, 17, 29, .62);
}
.domain-node-modes svg {
width: 19px;
color: var(--lake-accent);
}
.domain-node-modes strong,
.domain-node-modes small {
display: block;
}
.domain-node-modes strong { font-size: 12px; }
.domain-node-modes small { margin-top: 5px; color: var(--lake-muted); font-size: 9px; line-height: 1.5; }
.domain-connection-steps {
display: grid;
gap: 7px;
margin: 0;
padding: 0 20px;
list-style: none;
}
.domain-connection-steps li {
display: grid;
grid-template-columns: 32px minmax(0, 1fr) auto;
align-items: center;
gap: 11px;
padding: 11px 12px;
border: 1px solid var(--lake-border-soft);
border-radius: 10px;
background: rgba(9, 21, 34, .7);
}
.domain-connection-steps li > span {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(209, 154, 51, .12);
color: #d9ad5f;
font-size: 10px;
}
.domain-connection-steps li[data-state="verified"] > span {
background: rgba(81, 200, 120, .12);
color: var(--lake-success);
}
.domain-connection-steps svg { width: 15px; }
.domain-connection-steps strong,
.domain-connection-steps small { display: block; }
.domain-connection-steps strong { font-size: 11px; }
.domain-connection-steps small { margin-top: 3px; color: var(--lake-muted); font-size: 9px; line-height: 1.45; }
.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-connection-boundary {
margin-bottom: 18px;
padding: 10px 12px;
border-left: 2px solid var(--lake-accent);
background: var(--lake-accent-soft);
color: #a9c8d0;
font-size: 10px;
}
@media (max-width: 660px) {
.domain-node-modes { grid-template-columns: 1fr; }
.domain-connection-sheet .storage-sheet-footer { flex-wrap: wrap; }
}
.human-settings-backdrop {
position: fixed;
z-index: 110;