feat(hololake): add human-confirmed PNCC mounts

This commit is contained in:
冰朔 2026-08-13 14:01:43 +08:00
commit e70c9201c2
10 changed files with 524 additions and 15 deletions

View file

@ -659,6 +659,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.13.1",
"block2",
"libc",
"objc2",
]
@ -1353,6 +1355,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-updater",
"tauri-runtime",
"tauri-runtime-wry",
@ -2986,6 +2989,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@ -3867,6 +3894,48 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.20",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.20",
"toml 1.1.4+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"

View file

@ -25,6 +25,7 @@ tauri = { version = "=2.10.2", features = [] }
tauri-runtime = "=2.10.0"
tauri-runtime-wry = "=2.10.0"
tauri-plugin-updater = "2.10.0"
tauri-plugin-dialog = "=2.7.2"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]

View file

@ -17,6 +17,7 @@ pub fn run_connector() -> Result<(), String> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
home_status::get_hololake_home_status,
direct_local_session::issue_direct_local_discovery_ticket,
@ -27,6 +28,8 @@ pub fn run() {
local_development_bridge::inspect_development_write_lane,
local_development_bridge::release_development_write_lane,
pncc_repository_binding::inspect_mounted_pncc_repository,
pncc_repository_binding::select_pncc_repository_candidate,
pncc_repository_binding::confirm_pncc_repository_mount,
pncc_receipt_projection::query_pncc_receipt_projection,
])
.setup(|app| {

View file

@ -2,13 +2,19 @@
// Clean-room stage-one implementation. Contract evidence is recorded in
// audit/pncc-migration-provenance.json; no donor source was copied.
use fs2::FileExt;
use ring::digest::{digest, SHA256};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fs;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tauri_plugin_dialog::DialogExt;
use uuid::Uuid;
const MANIFEST_PATH: &str = ".hololake/persona/manifest.json";
const MANIFEST_SCHEMA: &str = "hololake.persona/v1";
@ -16,6 +22,8 @@ const MAX_MANIFEST_BYTES: usize = 512 * 1024;
const MAX_EVIDENCE_OBJECT_BYTES: usize = 2 * 1024 * 1024;
const MAX_DECLARED_ARTIFACTS: usize = 256;
const MOUNT_SCHEMA: &str = "hololake.pncc-stage-one-repository-mount/v1";
const CANDIDATE_SCHEMA: &str = "hololake.pncc-stage-one-repository-candidate/v1";
const CANDIDATE_TTL_MS: u128 = 15 * 60 * 1000;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@ -44,6 +52,41 @@ pub struct PnccRepositoryMountReceipt {
pub binding: PnccRepositoryInspectionReceipt,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfirmPnccRepositoryMountInput {
pub candidate_id: String,
pub confirmation_token: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct PnccRepositoryCandidateRecord {
schema: String,
state: String,
candidate_id: String,
confirmation_token_sha256: String,
repository_path: String,
expected_persona_id: String,
expected_human_responsibility_subject: String,
expected_head: String,
approved_receipt_id: String,
issued_at_unix_ms: u128,
expires_at_unix_ms: u128,
confirmed_mount_id: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PnccRepositoryCandidateReceipt {
pub schema: &'static str,
pub state: &'static str,
pub candidate_id: String,
pub confirmation_token: String,
pub expires_at_unix_ms: u128,
pub inspection: PnccRepositoryInspectionReceipt,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InspectPnccRepositoryInput {
@ -64,6 +107,51 @@ pub async fn inspect_mounted_pncc_repository(
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_JOIN_FAILED: {error}"))?
}
#[tauri::command]
pub async fn select_pncc_repository_candidate(
app: AppHandle,
) -> Result<Option<PnccRepositoryCandidateReceipt>, String> {
let dialog_app = app.clone();
let selected = tauri::async_runtime::spawn_blocking(move || {
dialog_app
.dialog()
.file()
.set_title("选择代码通道 Git 仓库")
.blocking_pick_folder()
})
.await
.map_err(|error| format!("PNCC_REPOSITORY_PICKER_JOIN_FAILED: {error}"))?;
let Some(selected) = selected else {
return Ok(None);
};
let repository = selected
.into_path()
.map_err(|error| format!("PNCC_REPOSITORY_PICKER_PATH_INVALID: {error}"))?;
let inspection =
tauri::async_runtime::spawn_blocking(move || inspect_repository_candidate(&repository))
.await
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_JOIN_FAILED: {error}"))??;
let candidate_root = pncc_repository_candidate_root(&app)?;
create_candidate_at(&candidate_root, inspection).map(Some)
}
#[tauri::command]
pub async fn confirm_pncc_repository_mount(
app: AppHandle,
input: ConfirmPnccRepositoryMountInput,
) -> Result<PnccRepositoryMountReceipt, String> {
let candidate_root = pncc_repository_candidate_root(&app)?;
let mount_root = pncc_repository_mount_root(&app)?;
let projection_root = crate::pncc_receipt_projection::pncc_projection_root(&app)?;
tauri::async_runtime::spawn_blocking(move || {
let receipt = confirm_candidate_at(&candidate_root, &mount_root, input)?;
crate::pncc_receipt_projection::append_repository_binding_at(&projection_root, &receipt)?;
Ok(receipt)
})
.await
.map_err(|error| format!("PNCC_REPOSITORY_CONFIRM_JOIN_FAILED: {error}"))?
}
pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
@ -77,6 +165,19 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))
}
fn pncc_repository_candidate_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
.join("pncc-stage-one-v1")
.join("repository-candidates");
fs::create_dir_all(&root)
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?;
root.canonicalize()
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))
}
pub(crate) fn mounted_repository_count_at(root: &Path) -> Result<usize, String> {
let mut count = 0;
for entry in fs::read_dir(root)
@ -103,6 +204,151 @@ pub(crate) fn mounted_repository_count_at(root: &Path) -> Result<usize, String>
Ok(count)
}
fn inspect_repository_candidate(
repository: &Path,
) -> Result<PnccRepositoryInspectionReceipt, String> {
let repository = exact_repository_root(repository)?;
let observed_head = git_text(&repository, &["rev-parse", "HEAD"], "PNCC_GIT_HEAD_READ")?
.trim()
.to_ascii_lowercase();
validate_head(&observed_head)?;
let (_, manifest_bytes) = read_committed_regular_blob(
&repository,
&observed_head,
MANIFEST_PATH,
MAX_MANIFEST_BYTES,
)?;
let manifest: PersonaManifestProjection = serde_json::from_slice(&manifest_bytes)
.map_err(|error| format!("PNCC_PERSONA_MANIFEST_INVALID: {error}"))?;
inspect_repository(InspectPnccRepositoryInput {
repository_path: repository.to_string_lossy().into_owned(),
expected_persona_id: manifest.persona_id,
expected_human_responsibility_subject: manifest.human_responsibility_subject,
expected_head: observed_head,
})
}
fn create_candidate_at(
root: &Path,
inspection: PnccRepositoryInspectionReceipt,
) -> Result<PnccRepositoryCandidateReceipt, String> {
let _lock = lock_candidates(root)?;
let issued_at_unix_ms = now_unix_ms()?;
let expires_at_unix_ms = issued_at_unix_ms + CANDIDATE_TTL_MS;
let candidate_id = format!("candidate-{}", Uuid::new_v4());
let confirmation_token = format!("confirm-{}-{}", Uuid::new_v4(), Uuid::new_v4());
let record = PnccRepositoryCandidateRecord {
schema: CANDIDATE_SCHEMA.into(),
state: "PENDING_HUMAN_CONFIRMATION".into(),
candidate_id: candidate_id.clone(),
confirmation_token_sha256: sha256_hex(confirmation_token.as_bytes()),
repository_path: inspection.repository_path.clone(),
expected_persona_id: inspection.persona_id.clone(),
expected_human_responsibility_subject: inspection.human_responsibility_subject.clone(),
expected_head: inspection.git_head.clone(),
approved_receipt_id: inspection.receipt_id.clone(),
issued_at_unix_ms,
expires_at_unix_ms,
confirmed_mount_id: None,
};
write_json_atomic(&candidate_path(root, &candidate_id), &record, "CANDIDATE")?;
Ok(PnccRepositoryCandidateReceipt {
schema: CANDIDATE_SCHEMA,
state: "AWAITING_HUMAN_CONFIRMATION",
candidate_id,
confirmation_token,
expires_at_unix_ms,
inspection,
})
}
fn confirm_candidate_at(
candidate_root: &Path,
mount_root: &Path,
input: ConfirmPnccRepositoryMountInput,
) -> Result<PnccRepositoryMountReceipt, String> {
validate_machine_id(&input.candidate_id, "CANDIDATE_ID")?;
if input.confirmation_token.len() < 32 || input.confirmation_token.len() > 256 {
return Err("PNCC_CONFIRMATION_TOKEN_INVALID".into());
}
let _lock = lock_candidates(candidate_root)?;
let path = candidate_path(candidate_root, &input.candidate_id);
let mut record: PnccRepositoryCandidateRecord = read_json_labeled(&path, "CANDIDATE")?;
if record.schema != CANDIDATE_SCHEMA || record.candidate_id != input.candidate_id {
return Err("PNCC_REPOSITORY_CANDIDATE_INVALID".into());
}
if record.confirmation_token_sha256 != sha256_hex(input.confirmation_token.as_bytes()) {
return Err("PNCC_REPOSITORY_CANDIDATE_NOT_AUTHORIZED".into());
}
if record.state == "CONFIRMED" {
let mount_id = record
.confirmed_mount_id
.ok_or("PNCC_REPOSITORY_CANDIDATE_INVALID")?;
return inspect_mounted_at(mount_root, InspectMountedPnccRepositoryInput { mount_id });
}
if record.state != "PENDING_HUMAN_CONFIRMATION" || now_unix_ms()? > record.expires_at_unix_ms {
return Err("PNCC_REPOSITORY_CANDIDATE_EXPIRED".into());
}
let binding = inspect_repository(InspectPnccRepositoryInput {
repository_path: record.repository_path.clone(),
expected_persona_id: record.expected_persona_id.clone(),
expected_human_responsibility_subject: record.expected_human_responsibility_subject.clone(),
expected_head: record.expected_head.clone(),
})?;
if binding.receipt_id != record.approved_receipt_id {
return Err("PNCC_REPOSITORY_CANDIDATE_EVIDENCE_DRIFT".into());
}
let mount_id = format!("pncc-{}", &binding.receipt_id[..16]);
let mount = PnccRepositoryMountRecord {
schema: MOUNT_SCHEMA.into(),
mount_id: mount_id.clone(),
repository_path: binding.repository_path.clone(),
expected_persona_id: binding.persona_id.clone(),
expected_human_responsibility_subject: binding.human_responsibility_subject.clone(),
expected_head: binding.git_head.clone(),
approved_receipt_id: binding.receipt_id.clone(),
};
let mount_record_path = mount_path(mount_root, &mount_id);
if mount_record_path.exists() {
let existing: PnccRepositoryMountRecord = read_json(&mount_record_path)?;
if existing.repository_path != mount.repository_path
|| existing.expected_head != mount.expected_head
|| existing.approved_receipt_id != mount.approved_receipt_id
{
return Err("PNCC_REPOSITORY_MOUNT_ID_CONFLICT".into());
}
} else {
write_json_atomic(&mount_record_path, &mount, "MOUNT")?;
}
record.state = "CONFIRMED".into();
record.confirmed_mount_id = Some(mount_id.clone());
write_json_atomic(&path, &record, "CANDIDATE")?;
Ok(PnccRepositoryMountReceipt {
schema: MOUNT_SCHEMA,
state: "HUMAN_CONFIRMED_READ_ONLY",
mount_id,
binding,
})
}
fn candidate_path(root: &Path, candidate_id: &str) -> PathBuf {
root.join(format!("{candidate_id}.json"))
}
fn lock_candidates(root: &Path) -> Result<std::fs::File, String> {
let lock = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(root.join("candidates.lock"))
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_LOCK_FAILED: {error}"))?;
lock.lock_exclusive()
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_LOCK_FAILED: {error}"))?;
Ok(lock)
}
pub(crate) fn inspect_mounted_at(
root: &Path,
input: InspectMountedPnccRepositoryInput,
@ -479,20 +725,53 @@ fn validate_machine_id(value: &str, label: &str) -> Result<(), String> {
}
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
read_json_labeled(path, "MOUNT")
}
fn read_json_labeled<T: for<'de> Deserialize<'de>>(path: &Path, label: &str) -> Result<T, String> {
let metadata = fs::symlink_metadata(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
"PNCC_REPOSITORY_MOUNT_NOT_FOUND".to_string()
format!("PNCC_REPOSITORY_{label}_NOT_FOUND")
} else {
format!("PNCC_REPOSITORY_MOUNT_READ_FAILED: {error}")
format!("PNCC_REPOSITORY_{label}_READ_FAILED: {error}")
}
})?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err("PNCC_REPOSITORY_MOUNT_RECORD_INVALID".into());
return Err(format!("PNCC_REPOSITORY_{label}_RECORD_INVALID"));
}
serde_json::from_slice(
&fs::read(path).map_err(|error| format!("PNCC_REPOSITORY_MOUNT_READ_FAILED: {error}"))?,
&fs::read(path).map_err(|error| format!("PNCC_REPOSITORY_{label}_READ_FAILED: {error}"))?,
)
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_RECORD_INVALID: {error}"))
.map_err(|error| format!("PNCC_REPOSITORY_{label}_RECORD_INVALID: {error}"))
}
fn write_json_atomic<T: Serialize>(path: &Path, value: &T, label: &str) -> Result<(), String> {
let parent = path
.parent()
.ok_or_else(|| format!("PNCC_REPOSITORY_{label}_PATH_INVALID"))?;
fs::create_dir_all(parent)
.map_err(|error| format!("PNCC_REPOSITORY_{label}_WRITE_FAILED: {error}"))?;
let temporary = parent.join(format!(".{label}-{}.tmp", Uuid::new_v4()));
let bytes = serde_json::to_vec_pretty(value)
.map_err(|error| format!("PNCC_REPOSITORY_{label}_RECORD_INVALID: {error}"))?;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&temporary)
.map_err(|error| format!("PNCC_REPOSITORY_{label}_WRITE_FAILED: {error}"))?;
file.write_all(&bytes)
.and_then(|_| file.sync_all())
.map_err(|error| format!("PNCC_REPOSITORY_{label}_WRITE_FAILED: {error}"))?;
fs::rename(&temporary, path)
.map_err(|error| format!("PNCC_REPOSITORY_{label}_WRITE_FAILED: {error}"))
}
fn now_unix_ms() -> Result<u128, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.map_err(|error| format!("PNCC_SYSTEM_CLOCK_INVALID: {error}"))
}
fn trusted_git() -> Command {
@ -690,4 +969,58 @@ mod tests {
"PNCC_REPOSITORY_RELATIVE_PATH_INVALID"
);
}
#[test]
fn human_confirmation_token_is_required_before_a_mount_is_written() {
let (_temp, repository, _head) = fixture();
let storage = TempDir::new().unwrap();
let candidates = storage.path().join("candidates");
let mounts = storage.path().join("mounts");
fs::create_dir_all(&candidates).unwrap();
fs::create_dir_all(&mounts).unwrap();
let candidate = create_candidate_at(
&candidates,
inspect_repository_candidate(&repository).unwrap(),
)
.unwrap();
assert!(fs::read_dir(&mounts).unwrap().next().is_none());
let wrong = confirm_candidate_at(
&candidates,
&mounts,
ConfirmPnccRepositoryMountInput {
candidate_id: candidate.candidate_id.clone(),
confirmation_token: "wrong-confirmation-token-long-enough-123".into(),
},
)
.unwrap_err();
assert_eq!(wrong, "PNCC_REPOSITORY_CANDIDATE_NOT_AUTHORIZED");
assert!(fs::read_dir(&mounts).unwrap().next().is_none());
let confirmed = confirm_candidate_at(
&candidates,
&mounts,
ConfirmPnccRepositoryMountInput {
candidate_id: candidate.candidate_id.clone(),
confirmation_token: candidate.confirmation_token.clone(),
},
)
.unwrap();
assert_eq!(confirmed.state, "HUMAN_CONFIRMED_READ_ONLY");
assert_eq!(confirmed.binding.persona_id, "ICE-P-ZY001");
assert!(!confirmed.binding.model_inference_started);
assert!(!confirmed.binding.reality_execution_allowed);
let retry = confirm_candidate_at(
&candidates,
&mounts,
ConfirmPnccRepositoryMountInput {
candidate_id: candidate.candidate_id,
confirmation_token: candidate.confirmation_token,
},
)
.unwrap();
assert_eq!(retry.mount_id, confirmed.mount_id);
assert_eq!(mounted_repository_count_at(&mounts).unwrap(), 1);
}
}