diff --git a/product-source/hololake-clean-desktop/contracts/enterprise-responsibility-entrance-v1.json b/product-source/hololake-clean-desktop/contracts/enterprise-responsibility-entrance-v1.json index 4fadb4113..1924f9ae9 100644 --- a/product-source/hololake-clean-desktop/contracts/enterprise-responsibility-entrance-v1.json +++ b/product-source/hololake-clean-desktop/contracts/enterprise-responsibility-entrance-v1.json @@ -1,7 +1,7 @@ { "schema": "hololake.enterprise-responsibility-entrance/v1", "contract_id": "HLP-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001", - "state": "ARCHITECTURE_REGISTERED_IMPLEMENTATION_PENDING", + "state": "LOCAL_DEVICE_PROOF_IMPLEMENTED_SERVER_CHALLENGE_DEFERRED", "surface": "HOLOLAKE_GATED_ENTERPRISE_PORTAL", "enterprise_server_embedded": false, "identities": ["DOMAIN_ID", "RESPONSIBLE_HUMAN_ID", "BOUND_PERSONA_ID", "LOCAL_NODE_ID", "DEVICE_KEY_ID"], @@ -10,4 +10,3 @@ "session": {"lifetime": "SHORT_LIVED", "scope": "ONE_DOMAIN_ONE_REPOSITORY", "replay_protection": true, "revocation_required": true}, "negative_cases": ["UNKNOWN_MEMBER", "WRONG_DOMAIN", "WRONG_PERSONA", "UNREGISTERED_DEVICE", "BAD_SIGNATURE", "REPLAY", "EXPIRED_SESSION", "CROSS_REPOSITORY_ACCESS"] } - diff --git a/product-source/hololake-clean-desktop/registries/module-registry.json b/product-source/hololake-clean-desktop/registries/module-registry.json index cf574f0f0..9ca7bd69f 100644 --- a/product-source/hololake-clean-desktop/registries/module-registry.json +++ b/product-source/hololake-clean-desktop/registries/module-registry.json @@ -61,8 +61,8 @@ "module_id": "HLP-MOD-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001", "name_zh": "光湖企业域责任入口", "kind": "TEAM_ONLY_ENTERPRISE_GATEWAY", - "state": "ARCHITECTURE_REGISTERED_IMPLEMENTATION_PENDING_SERVER_CONCURRENCY_DEFERRED", - "source": "contracts/enterprise-responsibility-entrance-v1.json", + "state": "LOCAL_DEVICE_PROOF_IMPLEMENTED_SERVER_CHALLENGE_DEFERRED_FOR_CONCURRENCY", + "source": "src-tauri/src/enterprise_entrance.rs", "enterprise_server_embedded": false, "required_proofs": ["DOMAIN_RESPONSIBLE_HUMAN_ID", "BOUND_PERSONA_ID", "LOCAL_DEVICE_KEY", "AUXILIARY_MACHINE_FINGERPRINT", "SERVER_CHALLENGE_SIGNATURE"], "session_scope": "ONE_DOMAIN_ONE_REPOSITORY_SHORT_LIVED", diff --git a/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py b/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py index 82e274c75..7b88ba576 100644 --- a/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py +++ b/product-source/hololake-clean-desktop/scripts/audit-clean-v1.py @@ -35,6 +35,7 @@ for required_file in ( "src-tauri/src/agent_executor.rs", "src-tauri/src/persona_runtime.rs", "src-tauri/src/realtime_bridge.rs", + "src-tauri/src/enterprise_entrance.rs", "connectors/hololake-glp-client.py", "contracts/public-runtime-v1.json", "contracts/clean-v1-execution-baseline.json", @@ -60,6 +61,8 @@ if enterprise.get("device_proof", {}).get("fingerprint_is_sole_credential") is n errors.append("machine fingerprint incorrectly used as sole credential") if enterprise.get("session", {}).get("scope") != "ONE_DOMAIN_ONE_REPOSITORY": errors.append("enterprise session is not responsibility scoped") +if enterprise.get("state") != "LOCAL_DEVICE_PROOF_IMPLEMENTED_SERVER_CHALLENGE_DEFERRED": + errors.append("enterprise local device proof implementation state drift") time_source = (ROOT / "src/time.ts").read_text() app_source = (ROOT / "src/App.tsx").read_text() if "legacyUnixSeconds" not in time_source or "时间待校验" not in time_source: diff --git a/product-source/hololake-clean-desktop/src-tauri/Cargo.lock b/product-source/hololake-clean-desktop/src-tauri/Cargo.lock index 1c4f63fdd..a98630e11 100644 --- a/product-source/hololake-clean-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-clean-desktop/src-tauri/Cargo.lock @@ -1330,8 +1330,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" name = "hololake-clean-desktop" version = "1.1.0" dependencies = [ + "base64 0.22.1", "chrono", "regex", + "ring", + "security-framework", "serde", "serde_json", "sha2", diff --git a/product-source/hololake-clean-desktop/src-tauri/Cargo.toml b/product-source/hololake-clean-desktop/src-tauri/Cargo.toml index c6811fe58..3d263b065 100644 --- a/product-source/hololake-clean-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-clean-desktop/src-tauri/Cargo.toml @@ -27,6 +27,11 @@ sha2 = "0.10" uuid = { version = "1", features = ["v4"] } regex = "1" chrono = { version = "0.4", default-features = false, features = ["clock"] } +base64 = "0.22" +ring = "0.17" + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "3" [dev-dependencies] tempfile = "3" diff --git a/product-source/hololake-clean-desktop/src-tauri/src/enterprise_entrance.rs b/product-source/hololake-clean-desktop/src-tauri/src/enterprise_entrance.rs new file mode 100644 index 000000000..56e00e4cc --- /dev/null +++ b/product-source/hololake-clean-desktop/src-tauri/src/enterprise_entrance.rs @@ -0,0 +1,321 @@ +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use regex::Regex; +use ring::{ + rand::SystemRandom, + signature::{Ed25519KeyPair, KeyPair}, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::process::Command; +use tauri::AppHandle; + +use crate::storage; + +const KEYCHAIN_SERVICE: &str = "world.guanghu.hololake.enterprise-device"; +const KEYCHAIN_ACCOUNT: &str = "responsibility-entrance-ed25519-v1"; +const ALLOWED_DOMAINS: [&str; 4] = ["DOMAIN-MAIN", "DOMAIN-SUB", "DOMAIN-ZERO", "DOMAIN-ZS"]; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnterpriseDeviceProof { + pub schema: String, + pub state: String, + pub node_id: String, + pub key_id: String, + pub public_key: String, + pub auxiliary_machine_fingerprint_sha256: String, + pub fingerprint_is_sole_credential: bool, + pub private_key_storage: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnterpriseBindingRequest { + pub schema: String, + pub request_id: String, + pub state: String, + pub domain_id: String, + pub responsible_human_id: String, + pub persona_id: String, + pub node_id: String, + pub key_id: String, + pub requested_repository_scope: String, + pub created_at: String, + pub server_authorized: bool, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnterpriseEntranceSnapshot { + pub schema: String, + pub state: String, + pub device: Option, + pub binding: Option, + pub enterprise_server_embedded: bool, + pub server_authorized: bool, + pub next_action: String, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnterpriseChallengeProof { + pub schema: String, + pub state: String, + pub request_id: String, + pub challenge_id: String, + pub node_id: String, + pub key_id: String, + pub public_key: String, + pub signature: String, + pub expires_unix_ms: u128, + pub server_authorized: bool, +} + +fn device_path(app: &AppHandle) -> Result { + Ok(storage::root(app)?.join("enterprise-entrance/device.json")) +} + +fn binding_path(app: &AppHandle) -> Result { + Ok(storage::root(app)?.join("enterprise-entrance/binding-request.json")) +} + +fn used_challenges_path(app: &AppHandle) -> Result { + Ok(storage::root(app)?.join("enterprise-entrance/used-challenges.json")) +} + +#[cfg(target_os = "macos")] +fn load_key_bytes() -> Result>, String> { + match security_framework::passwords::get_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT) { + Ok(value) => Ok(Some(value)), + Err(error) if error.code() == -25300 => Ok(None), + Err(error) => Err(format!("ENTERPRISE_DEVICE_KEYCHAIN_READ_FAILED: {error}")), + } +} + +#[cfg(target_os = "macos")] +fn save_key_bytes(value: &[u8]) -> Result<(), String> { + security_framework::passwords::set_generic_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, value) + .map_err(|error| format!("ENTERPRISE_DEVICE_KEYCHAIN_WRITE_FAILED: {error}")) +} + +#[cfg(not(target_os = "macos"))] +fn load_key_bytes() -> Result>, String> { + Err("ENTERPRISE_DEVICE_KEYCHAIN_UNAVAILABLE_ON_PLATFORM".into()) +} + +#[cfg(not(target_os = "macos"))] +fn save_key_bytes(_value: &[u8]) -> Result<(), String> { + Err("ENTERPRISE_DEVICE_KEYCHAIN_UNAVAILABLE_ON_PLATFORM".into()) +} + +fn load_or_create_key() -> Result { + let bytes = if let Some(value) = load_key_bytes()? { + value + } else { + let generated = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()) + .map_err(|_| "ENTERPRISE_DEVICE_KEY_GENERATION_FAILED".to_string())?; + save_key_bytes(generated.as_ref())?; + generated.as_ref().to_vec() + }; + Ed25519KeyPair::from_pkcs8(&bytes).map_err(|_| "ENTERPRISE_DEVICE_KEY_INVALID".to_string()) +} + +fn auxiliary_machine_fingerprint() -> String { + let mut material = format!("{}|{}", std::env::consts::OS, std::env::consts::ARCH); + for (program, args) in [ + ("ioreg", vec!["-rd1", "-c", "IOPlatformExpertDevice"]), + ("sysctl", vec!["-n", "kern.osversion"]), + ] { + if let Ok(output) = Command::new(program).args(args).output() { + if output.status.success() { + material.push('|'); + material.push_str(&String::from_utf8_lossy(&output.stdout)); + } + } + } + format!("{:x}", Sha256::digest(material.as_bytes())) +} + +fn validate_identity(value: &str, field: &str) -> Result { + let value = value.trim(); + let valid = Regex::new(r"^[A-Za-z0-9._∞-]{3,80}$").map_err(|e| e.to_string())?; + if !valid.is_match(value) { + return Err(format!("ENTERPRISE_{field}_INVALID")); + } + Ok(value.into()) +} + +fn canonical_challenge( + binding: &EnterpriseBindingRequest, + challenge_id: &str, + nonce: &str, + expires_unix_ms: u128, +) -> String { + format!( + "HLP-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001\n{}\n{}\n{}\n{}\n{}\n{}", + binding.request_id, + binding.domain_id, + binding.responsible_human_id, + binding.persona_id, + challenge_id, + format_args!("{nonce}:{expires_unix_ms}") + ) +} + +pub fn prepare_device(app: &AppHandle) -> Result { + let key = load_or_create_key()?; + let public = key.public_key().as_ref(); + let public_sha = format!("{:x}", Sha256::digest(public)); + let device = EnterpriseDeviceProof { + schema: "hololake.enterprise-device-proof/v1".into(), + state: "LOCAL_DEVICE_KEY_READY_SERVER_NOT_AUTHORIZED".into(), + node_id: format!("HL-NODE-{}", &public_sha[..16].to_uppercase()), + key_id: format!("HL-KEY-{}", &public_sha[..20].to_uppercase()), + public_key: BASE64.encode(public), + auxiliary_machine_fingerprint_sha256: auxiliary_machine_fingerprint(), + fingerprint_is_sole_credential: false, + private_key_storage: "MACOS_KEYCHAIN".into(), + created_at: storage::now(), + }; + storage::write_json(&device_path(app)?, &device)?; + Ok(device) +} + +pub fn prepare_binding( + app: &AppHandle, + domain_id: &str, + responsible_human_id: &str, + persona_id: &str, +) -> Result { + if !ALLOWED_DOMAINS.contains(&domain_id) { + return Err("ENTERPRISE_DOMAIN_ID_INVALID".into()); + } + let human = validate_identity(responsible_human_id, "RESPONSIBLE_HUMAN_ID")?; + let persona = validate_identity(persona_id, "PERSONA_ID")?; + let device: EnterpriseDeviceProof = storage::read_json(&device_path(app)?) + .map_err(|_| "ENTERPRISE_DEVICE_PROOF_REQUIRED".to_string())?; + let request = EnterpriseBindingRequest { + schema: "hololake.enterprise-binding-request/v1".into(), + request_id: storage::id("HL-ENT-REQ"), + state: "LOCAL_PROOF_READY_SERVER_CHALLENGE_REQUIRED".into(), + domain_id: domain_id.into(), + responsible_human_id: human, + persona_id: persona, + node_id: device.node_id, + key_id: device.key_id, + requested_repository_scope: format!("{domain_id}:ONE_RESPONSIBILITY_REPOSITORY"), + created_at: storage::now(), + server_authorized: false, + }; + storage::write_json(&binding_path(app)?, &request)?; + Ok(request) +} + +pub fn snapshot(app: &AppHandle) -> Result { + let device = device_path(app)?; + let binding = binding_path(app)?; + let device = if device.exists() { + Some(storage::read_json(&device)?) + } else { + None + }; + let binding = if binding.exists() { + Some(storage::read_json(&binding)?) + } else { + None + }; + let state = if binding.is_some() { + "LOCAL_PROOF_READY_SERVER_CHALLENGE_REQUIRED" + } else if device.is_some() { + "LOCAL_DEVICE_KEY_READY_BINDING_REQUIRED" + } else { + "LOCAL_DEVICE_REGISTRATION_REQUIRED" + }; + Ok(EnterpriseEntranceSnapshot { + schema: "hololake.enterprise-responsibility-entrance-snapshot/v1".into(), + state: state.into(), + device, + binding, + enterprise_server_embedded: false, + server_authorized: false, + next_action: "企业服务器并发任务结束后获取一次性挑战;本机证明不等于登录成功。".into(), + }) +} + +pub fn sign_challenge( + app: &AppHandle, + request_id: &str, + challenge_id: &str, + nonce: &str, + expires_unix_ms: u128, +) -> Result { + let binding: EnterpriseBindingRequest = storage::read_json(&binding_path(app)?)?; + if binding.request_id != request_id { + return Err("ENTERPRISE_BINDING_REQUEST_MISMATCH".into()); + } + let challenge_id = validate_identity(challenge_id, "CHALLENGE_ID")?; + let nonce = validate_identity(nonce, "CHALLENGE_NONCE")?; + let now = storage::now_unix_ms(); + if expires_unix_ms <= now || expires_unix_ms > now + 5 * 60 * 1000 { + return Err("ENTERPRISE_CHALLENGE_EXPIRY_INVALID".into()); + } + let used_path = used_challenges_path(app)?; + let mut used: Vec = if used_path.exists() { + storage::read_json(&used_path)? + } else { + vec![] + }; + if used.contains(&challenge_id) { + return Err("ENTERPRISE_CHALLENGE_REPLAYED".into()); + } + let key = load_or_create_key()?; + let signature = + key.sign(canonical_challenge(&binding, &challenge_id, &nonce, expires_unix_ms).as_bytes()); + used.push(challenge_id.clone()); + storage::write_json(&used_path, &used)?; + Ok(EnterpriseChallengeProof { + schema: "hololake.enterprise-challenge-proof/v1".into(), + state: "SIGNED_SERVER_VERIFICATION_REQUIRED".into(), + request_id: binding.request_id, + challenge_id, + node_id: binding.node_id, + key_id: binding.key_id, + public_key: BASE64.encode(key.public_key().as_ref()), + signature: BASE64.encode(signature.as_ref()), + expires_unix_ms, + server_authorized: false, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_unknown_domain_and_invalid_identity() { + assert!(!ALLOWED_DOMAINS.contains(&"DOMAIN-FIFTH")); + assert!(validate_identity("../human", "RESPONSIBLE_HUMAN_ID").is_err()); + assert!(validate_identity("ICE-GL∞", "RESPONSIBLE_HUMAN_ID").is_ok()); + } + + #[test] + fn canonical_challenge_binds_domain_human_persona_and_expiry() { + let binding = EnterpriseBindingRequest { + schema: "test".into(), + request_id: "REQ-001".into(), + state: "test".into(), + domain_id: "DOMAIN-MAIN".into(), + responsible_human_id: "HUMAN-001".into(), + persona_id: "PERSONA-001".into(), + node_id: "NODE-001".into(), + key_id: "KEY-001".into(), + requested_repository_scope: "DOMAIN-MAIN:ONE_RESPONSIBILITY_REPOSITORY".into(), + created_at: "2026-09-03T00:00:00.000Z".into(), + server_authorized: false, + }; + let value = canonical_challenge(&binding, "CHALLENGE-001", "NONCE-001", 123456); + assert!(value.contains("DOMAIN-MAIN\nHUMAN-001\nPERSONA-001")); + assert!(value.ends_with("NONCE-001:123456")); + } +} diff --git a/product-source/hololake-clean-desktop/src-tauri/src/lib.rs b/product-source/hololake-clean-desktop/src-tauri/src/lib.rs index 417b75edb..3a41d72d1 100644 --- a/product-source/hololake-clean-desktop/src-tauri/src/lib.rs +++ b/product-source/hololake-clean-desktop/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod agent_executor; +mod enterprise_entrance; mod model; mod persona_runtime; mod realtime_bridge; @@ -456,6 +457,41 @@ fn reject_agent_proposal( Ok(proposal) } +#[tauri::command] +fn enterprise_entrance_snapshot( + app: AppHandle, +) -> Result { + enterprise_entrance::snapshot(&app) +} + +#[tauri::command] +fn prepare_enterprise_device( + app: AppHandle, +) -> Result { + enterprise_entrance::prepare_device(&app) +} + +#[tauri::command] +fn prepare_enterprise_binding( + app: AppHandle, + domain_id: String, + responsible_human_id: String, + persona_id: String, +) -> Result { + enterprise_entrance::prepare_binding(&app, &domain_id, &responsible_human_id, &persona_id) +} + +#[tauri::command] +fn sign_enterprise_challenge( + app: AppHandle, + request_id: String, + challenge_id: String, + nonce: String, + expires_unix_ms: u128, +) -> Result { + enterprise_entrance::sign_challenge(&app, &request_id, &challenge_id, &nonce, expires_unix_ms) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -533,6 +569,10 @@ pub fn run() { compile_tcs_agent_proposal, approve_agent_proposal, reject_agent_proposal + ,enterprise_entrance_snapshot + ,prepare_enterprise_device + ,prepare_enterprise_binding + ,sign_enterprise_challenge ]) .run(tauri::generate_context!()) .expect("HoloLake runtime failed") diff --git a/product-source/hololake-clean-desktop/src/App.tsx b/product-source/hololake-clean-desktop/src/App.tsx index 52a3a4c52..1c903b694 100644 --- a/product-source/hololake-clean-desktop/src/App.tsx +++ b/product-source/hololake-clean-desktop/src/App.tsx @@ -10,6 +10,7 @@ import type { SourceKind, RuntimeOverview, SystemSnapshot, + EnterpriseEntranceSnapshot, } from "./types"; import { MarkdownDocument, @@ -132,7 +133,7 @@ export default function App() { {view === "knowledge" && }{" "} {view === "market" && }{" "} {view === "history" && }{" "} - {view === "portal" && }{" "} + {view === "portal" && }{" "} {view === "settings" && ( ); } -function Portal() { +function Portal({ setError }: { setError: (value: string) => void }) { + const [entrance, setEntrance] = useState(null); + const [domainId, setDomainId] = useState("DOMAIN-MAIN"); + const [humanId, setHumanId] = useState(""); + const [personaId, setPersonaId] = useState(""); + const [busy, setBusy] = useState(false); + const refresh = () => api.enterpriseEntranceSnapshot().then(setEntrance).catch((error) => setError(String(error))); + useEffect(() => { + void refresh(); + }, []); + const prepareDevice = async () => { + setBusy(true); + try { + await api.prepareEnterpriseDevice(); + await refresh(); + } catch (error) { + setError(String(error)); + } finally { + setBusy(false); + } + }; + const prepareBinding = async () => { + setBusy(true); + try { + await api.prepareEnterpriseBinding(domainId, humanId, personaId); + await refresh(); + } catch (error) { + setError(String(error)); + } finally { + setBusy(false); + } + }; return (
@@ -906,24 +938,61 @@ function Portal() {
-

公共内容不嵌入私人频道

+

公共内容与企业责任分开进入

官方通知、四域规则和团队发布由企业门户承载。HoloLake - 只读取经过签名审核的公共发行事实。 + 只读取经过签名审核的公共发行事实;企业服务器和四域系统本体不会进入你的私人频道。

+
+
+
+ HLP-MOD-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001 +

光湖企业域责任入口

+

仅为已登记的域负责人准备本机证明。准备完成不等于企业服务器已经授权。

+
+ + {entrance?.serverAuthorized ? "企业会话已验证" : "企业服务器未授权"} + +
+
+
+ 本机设备证明 +

{entrance?.device ? `${entrance.device.nodeId} · ${entrance.device.keyId}` : "尚未在系统钥匙串生成设备密钥"}

+ 机器指纹仅作辅助证据,不能单独登录。 + {!entrance?.device && } +
+
+ 责任绑定 + {entrance?.binding ? ( + <> +

{entrance.binding.domainId} · {entrance.binding.responsibleHumanId} · {entrance.binding.personaId}

+ {entrance.binding.state} + + ) : ( +
+ + setHumanId(event.target.value)} placeholder="域负责人编号" /> + setPersonaId(event.target.value)} placeholder="绑定人格体编号" /> + +
+ )} +
+
+
{entrance?.nextAction ?? "正在读取本机责任入口状态…"}
+
); } diff --git a/product-source/hololake-clean-desktop/src/runtime.ts b/product-source/hololake-clean-desktop/src/runtime.ts index ac79ff37c..632a9890b 100644 --- a/product-source/hololake-clean-desktop/src/runtime.ts +++ b/product-source/hololake-clean-desktop/src/runtime.ts @@ -11,6 +11,9 @@ import type { RuntimeOverview, SystemSnapshot, TimelineEvent, + EnterpriseBindingRequest, + EnterpriseDeviceProof, + EnterpriseEntranceSnapshot, } from "./types"; const tauri = () => "__TAURI_INTERNALS__" in window; @@ -291,3 +294,30 @@ export async function rejectProposal( ): Promise { return invoke("reject_agent_proposal", { proposalId }); } + +export async function enterpriseEntranceSnapshot(): Promise { + if (tauri()) return invoke("enterprise_entrance_snapshot"); + return { + schema: "preview", + state: "LOCAL_DEVICE_REGISTRATION_REQUIRED", + device: null, + binding: null, + enterpriseServerEmbedded: false, + serverAuthorized: false, + nextAction: "浏览器预览不生成设备凭证。", + }; +} + +export async function prepareEnterpriseDevice(): Promise { + if (!tauri()) throw new Error("设备密钥只能在已安装的 HoloLake 中生成"); + return invoke("prepare_enterprise_device"); +} + +export async function prepareEnterpriseBinding( + domainId: string, + responsibleHumanId: string, + personaId: string, +): Promise { + if (!tauri()) throw new Error("企业责任绑定只能在已安装的 HoloLake 中准备"); + return invoke("prepare_enterprise_binding", { domainId, responsibleHumanId, personaId }); +} diff --git a/product-source/hololake-clean-desktop/src/styles.css b/product-source/hololake-clean-desktop/src/styles.css index 464f802f0..9fbd7ea2c 100644 --- a/product-source/hololake-clean-desktop/src/styles.css +++ b/product-source/hololake-clean-desktop/src/styles.css @@ -980,6 +980,86 @@ main { width: 72px; height: 72px; } +.enterprise-entrance { + margin-top: 18px; + padding: 24px; + border: 1px solid rgba(116, 174, 213, 0.2); + border-radius: 18px; + background: + radial-gradient(circle at 78% 0%, rgba(94, 163, 207, 0.1), transparent 34%), + rgba(6, 20, 36, 0.82); +} +.enterprise-entrance > header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + padding: 0; +} +.enterprise-entrance h2 { + margin: 7px 0 5px; + font-size: 20px; +} +.enterprise-entrance p, +.enterprise-entrance small, +.enterprise-entrance footer { + color: var(--muted); + line-height: 1.6; +} +.enterprise-entrance .eyebrow { + color: #8ebde0; + font-size: 10px; + font-weight: 650; + letter-spacing: 0.08em; +} +.status.pending { + color: #e4ba67; +} +.enterprise-proof-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 20px; +} +.enterprise-proof-grid article { + display: grid; + align-content: start; + gap: 9px; + min-height: 176px; + padding: 18px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.035); +} +.enterprise-proof-grid article > strong { + font-size: 15px; +} +.enterprise-proof-grid article p, +.enterprise-proof-grid article small { + margin: 0; + overflow-wrap: anywhere; +} +.enterprise-proof-grid button { + justify-self: start; + margin-top: auto; +} +.enterprise-binding-form { + display: grid; + gap: 8px; +} +.enterprise-binding-form input, +.enterprise-binding-form select { + width: 100%; + min-height: 39px; + border: 1px solid var(--line); + border-radius: 10px; + padding: 0 12px; + color: var(--text); + background: rgba(2, 12, 23, 0.72); +} +.enterprise-entrance footer { + margin-top: 16px; + font-size: 12px; +} .origin-light { border-radius: 50%; background: radial-gradient( diff --git a/product-source/hololake-clean-desktop/src/types.ts b/product-source/hololake-clean-desktop/src/types.ts index 39b1ffd5d..799af3114 100644 --- a/product-source/hololake-clean-desktop/src/types.ts +++ b/product-source/hololake-clean-desktop/src/types.ts @@ -126,3 +126,36 @@ export interface RealtimeInvitation { connectorCommand: string; warning: string; } +export interface EnterpriseDeviceProof { + schema: string; + state: string; + nodeId: string; + keyId: string; + publicKey: string; + auxiliaryMachineFingerprintSha256: string; + fingerprintIsSoleCredential: boolean; + privateKeyStorage: string; + createdAt: string; +} +export interface EnterpriseBindingRequest { + schema: string; + requestId: string; + state: string; + domainId: string; + responsibleHumanId: string; + personaId: string; + nodeId: string; + keyId: string; + requestedRepositoryScope: string; + createdAt: string; + serverAuthorized: boolean; +} +export interface EnterpriseEntranceSnapshot { + schema: string; + state: string; + device: EnterpriseDeviceProof | null; + binding: EnterpriseBindingRequest | null; + enterpriseServerEmbedded: boolean; + serverAuthorized: boolean; + nextAction: string; +} diff --git a/routing/hololake-current-architecture.json b/routing/hololake-current-architecture.json index b7b162215..94bae0727 100644 --- a/routing/hololake-current-architecture.json +++ b/routing/hololake-current-architecture.json @@ -25,7 +25,7 @@ "bingshuo_enterprise_language_master_work_repository": "REPO-016:bingshuo/guanghu-language-master-work", "team_enterprise_login": "https://guanghu.chat/code/user/login", "enterprise_responsibility_entrance": "HLP-MOD-ENTERPRISE-RESPONSIBILITY-ENTRANCE-0001", - "enterprise_responsibility_entrance_state": "ARCHITECTURE_REGISTERED_IMPLEMENTATION_PENDING_SERVER_CONCURRENCY_DEFERRED", + "enterprise_responsibility_entrance_state": "LOCAL_DEVICE_PROOF_IMPLEMENTED_SERVER_CHALLENGE_DEFERRED_FOR_CONCURRENCY", "enterprise_server_embedded": false, "enterprise_login_required_proofs": [ "DOMAIN_RESPONSIBLE_HUMAN_ID",