955 lines
32 KiB
Rust
955 lines
32 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// 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::fs::{self, OpenOptions};
|
|
use std::io::Write;
|
|
use std::path::{Component, Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
use uuid::Uuid;
|
|
|
|
const REMOTE_MOUNT_SCHEMA: &str = "hololake.pncc-stage-one-remote-mount/v1";
|
|
const CURSOR_SCHEMA: &str = "hololake.pncc-stage-one-remote-cursor/v1";
|
|
#[cfg(test)]
|
|
const DEFAULT_BRANCH: &str = "main";
|
|
const DEFAULT_CACHE_LIMIT_BYTES: u64 = 128 * 1024 * 1024;
|
|
const MIN_CACHE_LIMIT_BYTES: u64 = 1024 * 1024;
|
|
const MAX_CACHE_LIMIT_BYTES: u64 = 4 * 1024 * 1024 * 1024;
|
|
const MAX_TEXT_OBJECT_BYTES: usize = 2 * 1024 * 1024;
|
|
const MAX_CONTINUITY_DEPTH: usize = 4096;
|
|
|
|
#[cfg(test)]
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct RegisterPnccRemoteMountInput {
|
|
pub mount_id: String,
|
|
pub remote_url: String,
|
|
pub branch: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
pub struct ReadMountedPnccRemoteObjectInput {
|
|
pub mount_id: String,
|
|
pub relative_path: String,
|
|
pub expected_head: Option<String>,
|
|
pub max_cache_bytes: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PnccRemoteObjectReceipt {
|
|
pub schema: &'static str,
|
|
pub mount_id: String,
|
|
pub remote_identity_hash: String,
|
|
pub remote_head: String,
|
|
pub previous_verified_head: Option<String>,
|
|
pub cursor_advanced: bool,
|
|
pub continuity_verified: bool,
|
|
pub fetched_incremental_objects: bool,
|
|
pub cache_rehydrated: bool,
|
|
pub relative_path: String,
|
|
pub git_object_id: String,
|
|
pub content: String,
|
|
pub content_sha256: String,
|
|
pub cache_bytes: u64,
|
|
pub cache_limit_bytes: u64,
|
|
pub cache_state: &'static str,
|
|
pub worktree_created: bool,
|
|
pub full_history_requested: bool,
|
|
pub model_inference_started: bool,
|
|
pub reality_execution_allowed: bool,
|
|
pub receipt_id: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct RemoteMountRecord {
|
|
schema: String,
|
|
mount_id: String,
|
|
remote_url: String,
|
|
remote_identity_hash: String,
|
|
branch: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct CursorRecord {
|
|
schema: String,
|
|
remote_identity_hash: String,
|
|
branch: String,
|
|
verified_head: String,
|
|
previous_verified_head: Option<String>,
|
|
generation: u64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn register_remote_mount_at(
|
|
root: &Path,
|
|
input: RegisterPnccRemoteMountInput,
|
|
allow_file_remote: bool,
|
|
) -> Result<(), String> {
|
|
validate_machine_id(&input.mount_id, "REMOTE_MOUNT_ID")?;
|
|
validate_remote_url(&input.remote_url, allow_file_remote)?;
|
|
let branch = input.branch.unwrap_or_else(|| DEFAULT_BRANCH.into());
|
|
validate_branch(&branch)?;
|
|
let identity = remote_identity(&input.remote_url, &branch);
|
|
let record = RemoteMountRecord {
|
|
schema: REMOTE_MOUNT_SCHEMA.into(),
|
|
mount_id: input.mount_id.clone(),
|
|
remote_url: input.remote_url,
|
|
remote_identity_hash: identity.clone(),
|
|
branch: branch.clone(),
|
|
};
|
|
let mounts = child_root(root, "mounts")?;
|
|
write_json_atomic(&mounts.join(format!("{}.json", input.mount_id)), &record)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn read_mounted_remote_object_at(
|
|
root: &Path,
|
|
input: ReadMountedPnccRemoteObjectInput,
|
|
) -> Result<PnccRemoteObjectReceipt, String> {
|
|
read_mounted_remote_object_with_file_policy(root, input, false)
|
|
}
|
|
|
|
fn read_mounted_remote_object_with_file_policy(
|
|
root: &Path,
|
|
input: ReadMountedPnccRemoteObjectInput,
|
|
allow_file_remote: bool,
|
|
) -> Result<PnccRemoteObjectReceipt, String> {
|
|
validate_machine_id(&input.mount_id, "REMOTE_MOUNT_ID")?;
|
|
validate_relative_path(&input.relative_path)?;
|
|
let maximum = input.max_cache_bytes.unwrap_or(DEFAULT_CACHE_LIMIT_BYTES);
|
|
if !(MIN_CACHE_LIMIT_BYTES..=MAX_CACHE_LIMIT_BYTES).contains(&maximum) {
|
|
return Err("PNCC_REMOTE_CACHE_LIMIT_INVALID".into());
|
|
}
|
|
let expected_head = input
|
|
.expected_head
|
|
.map(|head| validate_head(&head))
|
|
.transpose()?;
|
|
let mounts = child_root(root, "mounts")?;
|
|
let record: RemoteMountRecord = read_json(&mounts.join(format!("{}.json", input.mount_id)))?;
|
|
if record.schema != REMOTE_MOUNT_SCHEMA || record.mount_id != input.mount_id {
|
|
return Err("PNCC_REMOTE_MOUNT_RECORD_INVALID".into());
|
|
}
|
|
validate_remote_url(&record.remote_url, allow_file_remote)?;
|
|
validate_branch(&record.branch)?;
|
|
let identity = remote_identity(&record.remote_url, &record.branch);
|
|
if identity != record.remote_identity_hash {
|
|
return Err("PNCC_REMOTE_MOUNT_IDENTITY_MISMATCH".into());
|
|
}
|
|
|
|
let locks = child_root(root, "locks")?;
|
|
let _lock = lock_file(&locks.join(format!("{identity}.lock")))?;
|
|
let remote_head = read_remote_head(&record.remote_url, &record.branch)?;
|
|
if expected_head
|
|
.as_deref()
|
|
.is_some_and(|head| head != remote_head)
|
|
{
|
|
return Err("PNCC_REMOTE_HEAD_MISMATCH".into());
|
|
}
|
|
|
|
let object_root = child_root(root, "objects")?;
|
|
let cursor_root = child_root(root, "continuity")?;
|
|
let cache_path = object_root.join(format!("{identity}.git"));
|
|
let cursor_path = cursor_root.join(format!("{identity}.json"));
|
|
let cursor = read_cursor(&cursor_path, &identity, &record.branch)?;
|
|
let previous = cursor.as_ref().map(|value| value.verified_head.clone());
|
|
|
|
ensure_bare_cache(&cache_path, &record.remote_url)?;
|
|
let remote_ref = format!("refs/remotes/origin/{}", record.branch);
|
|
let cached = optional_ref(&cache_path, &remote_ref)?;
|
|
match (previous.as_deref(), cached.as_deref()) {
|
|
(None, Some(_)) => return Err("PNCC_REMOTE_CACHE_WITHOUT_CONTINUITY_CURSOR".into()),
|
|
(Some(expected), Some(observed)) if expected != observed => {
|
|
return Err("PNCC_REMOTE_CACHE_CURSOR_MISMATCH".into())
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let fetched_incremental_objects = cached.as_deref() != Some(remote_head.as_str());
|
|
let cache_rehydrated = cached.is_none() && previous.is_some();
|
|
if fetched_incremental_objects {
|
|
if let Err(error) = fetch_continuity(
|
|
&cache_path,
|
|
&record.branch,
|
|
previous.as_deref(),
|
|
cached.is_some(),
|
|
) {
|
|
if cache_path.exists() {
|
|
remove_exact_cache(&object_root, &cache_path)?;
|
|
}
|
|
return Err(error);
|
|
}
|
|
}
|
|
let fetched_head = required_ref(&cache_path, &remote_ref)?;
|
|
if fetched_head != remote_head {
|
|
return Err("PNCC_REMOTE_FETCH_HEAD_MISMATCH".into());
|
|
}
|
|
if let Some(previous_head) = previous.as_deref() {
|
|
if previous_head != remote_head && !is_ancestor(&cache_path, previous_head, &remote_head)? {
|
|
remove_exact_cache(&object_root, &cache_path)?;
|
|
return Err("PNCC_REMOTE_HISTORY_REWRITE_REJECTED".into());
|
|
}
|
|
}
|
|
|
|
let (object_id, content_bytes) = read_regular_blob(
|
|
&cache_path,
|
|
&remote_head,
|
|
&input.relative_path,
|
|
MAX_TEXT_OBJECT_BYTES,
|
|
)?;
|
|
let content =
|
|
String::from_utf8(content_bytes).map_err(|_| "PNCC_REMOTE_OBJECT_NOT_UTF8".to_string())?;
|
|
let content_sha256 = sha256_hex(content.as_bytes());
|
|
let cursor_advanced = previous.as_deref() != Some(remote_head.as_str());
|
|
let next = CursorRecord {
|
|
schema: CURSOR_SCHEMA.into(),
|
|
remote_identity_hash: identity.clone(),
|
|
branch: record.branch,
|
|
verified_head: remote_head.clone(),
|
|
previous_verified_head: previous.clone(),
|
|
generation: cursor
|
|
.as_ref()
|
|
.map(|value| value.generation.saturating_add(u64::from(cursor_advanced)))
|
|
.unwrap_or(1),
|
|
};
|
|
write_json_atomic(&cursor_path, &next)?;
|
|
|
|
let observed_cache_bytes = directory_size(&cache_path)?;
|
|
let (cache_bytes, cache_state) = if observed_cache_bytes > maximum {
|
|
remove_exact_cache(&object_root, &cache_path)?;
|
|
(0, "EVICTED_AFTER_BOUNDED_READ")
|
|
} else {
|
|
(observed_cache_bytes, "BOUNDED_PARTIAL_OBJECT_CACHE")
|
|
};
|
|
let receipt_id = sha256_hex(
|
|
format!(
|
|
"{}\n{}\n{}\n{}\n{}",
|
|
input.mount_id, remote_head, input.relative_path, object_id, content_sha256
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
Ok(PnccRemoteObjectReceipt {
|
|
schema: "hololake.pncc-stage-one-remote-object-read/v1",
|
|
mount_id: input.mount_id,
|
|
remote_identity_hash: identity,
|
|
remote_head,
|
|
previous_verified_head: previous,
|
|
cursor_advanced,
|
|
continuity_verified: true,
|
|
fetched_incremental_objects,
|
|
cache_rehydrated,
|
|
relative_path: input.relative_path,
|
|
git_object_id: object_id,
|
|
content,
|
|
content_sha256,
|
|
cache_bytes,
|
|
cache_limit_bytes: maximum,
|
|
cache_state,
|
|
worktree_created: false,
|
|
full_history_requested: false,
|
|
model_inference_started: false,
|
|
reality_execution_allowed: false,
|
|
receipt_id,
|
|
})
|
|
}
|
|
|
|
fn validate_remote_url(remote_url: &str, allow_file_remote: bool) -> Result<(), String> {
|
|
if remote_url.len() > 4096 || remote_url.contains(['\n', '\r', '\0']) {
|
|
return Err("PNCC_REMOTE_URL_UNSUPPORTED".into());
|
|
}
|
|
if allow_file_remote && remote_url.starts_with("file://") {
|
|
return Ok(());
|
|
}
|
|
let url =
|
|
tauri::Url::parse(remote_url).map_err(|_| "PNCC_REMOTE_URL_UNSUPPORTED".to_string())?;
|
|
if url.scheme() != "https"
|
|
|| url.host_str().is_none()
|
|
|| !url.username().is_empty()
|
|
|| url.password().is_some()
|
|
|| url.query().is_some()
|
|
|| url.fragment().is_some()
|
|
{
|
|
return Err("PNCC_REMOTE_URL_MUST_BE_CREDENTIAL_FREE_HTTPS".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_branch(branch: &str) -> Result<(), String> {
|
|
if branch.is_empty() || branch.len() > 200 || branch.starts_with('-') {
|
|
return Err("PNCC_REMOTE_BRANCH_INVALID".into());
|
|
}
|
|
let output = trusted_git()
|
|
.args(["check-ref-format", "--branch", branch])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_GIT_UNAVAILABLE: {error}"))?;
|
|
if output.status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("PNCC_REMOTE_BRANCH_INVALID".into())
|
|
}
|
|
}
|
|
|
|
fn validate_relative_path(relative: &str) -> Result<(), String> {
|
|
let path = Path::new(relative);
|
|
if relative.is_empty()
|
|
|| relative.len() > 1024
|
|
|| path.is_absolute()
|
|
|| path.components().any(|component| {
|
|
matches!(
|
|
component,
|
|
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
|
)
|
|
})
|
|
|| path
|
|
.components()
|
|
.any(|component| component.as_os_str() == ".git")
|
|
{
|
|
return Err("PNCC_REMOTE_PATH_INVALID".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_head(head: &str) -> Result<String, String> {
|
|
if head.len() == 40
|
|
&& head
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
|
{
|
|
Ok(head.into())
|
|
} else {
|
|
Err("PNCC_REMOTE_HEAD_INVALID".into())
|
|
}
|
|
}
|
|
|
|
fn validate_machine_id(value: &str, label: &str) -> Result<(), String> {
|
|
if value.is_empty()
|
|
|| value.len() > 128
|
|
|| !value
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
|
|
{
|
|
return Err(format!("PNCC_{label}_INVALID"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn remote_identity(remote_url: &str, branch: &str) -> String {
|
|
sha256_hex(format!("{remote_url}\n{branch}").as_bytes())
|
|
}
|
|
|
|
fn read_remote_head(remote_url: &str, branch: &str) -> Result<String, String> {
|
|
let full_ref = format!("refs/heads/{branch}");
|
|
let output = trusted_git()
|
|
.env("GIT_TRACE_PACKET", "1")
|
|
.args([
|
|
"-c",
|
|
"protocol.version=2",
|
|
"ls-remote",
|
|
"--exit-code",
|
|
"--heads",
|
|
remote_url,
|
|
&full_ref,
|
|
])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_REMOTE_HEAD_READ_FAILED: {error}"))?;
|
|
let output = require_success(output, "PNCC_REMOTE_HEAD_READ")?;
|
|
let trace = String::from_utf8_lossy(&output.stderr);
|
|
if !trace.lines().any(|line| {
|
|
line.split_whitespace().any(|word| word == "filter")
|
|
&& line
|
|
.split_whitespace()
|
|
.any(|word| word.starts_with("fetch="))
|
|
}) {
|
|
return Err("PNCC_REMOTE_PARTIAL_OBJECT_PROTOCOL_REQUIRED".into());
|
|
}
|
|
let response = String::from_utf8(output.stdout)
|
|
.map_err(|_| "PNCC_REMOTE_HEAD_RESPONSE_NOT_UTF8".to_string())?;
|
|
let mut lines = response.lines();
|
|
let first = lines
|
|
.next()
|
|
.ok_or_else(|| "PNCC_REMOTE_BRANCH_NOT_FOUND".to_string())?;
|
|
if lines.next().is_some() {
|
|
return Err("PNCC_REMOTE_HEAD_AMBIGUOUS".into());
|
|
}
|
|
let mut fields = first.split_whitespace();
|
|
let head = fields.next().unwrap_or_default().to_ascii_lowercase();
|
|
if fields.next() != Some(full_ref.as_str()) || fields.next().is_some() {
|
|
return Err("PNCC_REMOTE_HEAD_RESPONSE_INVALID".into());
|
|
}
|
|
validate_head(&head)
|
|
}
|
|
|
|
fn ensure_bare_cache(cache: &Path, remote_url: &str) -> Result<(), String> {
|
|
if cache.exists() {
|
|
if git_text(
|
|
cache,
|
|
&["rev-parse", "--is-bare-repository"],
|
|
"PNCC_CACHE_PROBE",
|
|
)?
|
|
.trim()
|
|
!= "true"
|
|
{
|
|
return Err("PNCC_REMOTE_CACHE_NOT_BARE".into());
|
|
}
|
|
if git_text(
|
|
cache,
|
|
&["remote", "get-url", "origin"],
|
|
"PNCC_CACHE_ORIGIN_READ",
|
|
)?
|
|
.trim()
|
|
!= remote_url
|
|
{
|
|
return Err("PNCC_REMOTE_CACHE_ORIGIN_MISMATCH".into());
|
|
}
|
|
return Ok(());
|
|
}
|
|
fs::create_dir(cache).map_err(|error| format!("PNCC_REMOTE_CACHE_CREATE_FAILED: {error}"))?;
|
|
git_status(
|
|
trusted_git().args(["init", "--bare", &cache.to_string_lossy()]),
|
|
"PNCC_REMOTE_CACHE_INIT",
|
|
)?;
|
|
git_status(
|
|
git_at(cache).args(["remote", "add", "origin", remote_url]),
|
|
"PNCC_REMOTE_CACHE_ORIGIN_ADD",
|
|
)?;
|
|
git_status(
|
|
git_at(cache).args(["config", "remote.origin.promisor", "true"]),
|
|
"PNCC_REMOTE_CACHE_PROMISOR_CONFIG",
|
|
)?;
|
|
git_status(
|
|
git_at(cache).args(["config", "remote.origin.partialclonefilter", "blob:none"]),
|
|
"PNCC_REMOTE_CACHE_FILTER_CONFIG",
|
|
)
|
|
}
|
|
|
|
fn fetch_continuity(
|
|
cache: &Path,
|
|
branch: &str,
|
|
previous: Option<&str>,
|
|
cache_has_previous: bool,
|
|
) -> Result<(), String> {
|
|
let refspec = format!("+refs/heads/{branch}:refs/remotes/origin/{branch}");
|
|
if previous.is_none() || !cache_has_previous {
|
|
fetch_depth(cache, &refspec, 1)?;
|
|
} else {
|
|
fetch_incremental(cache, &refspec)?;
|
|
}
|
|
let Some(previous) = previous else {
|
|
return Ok(());
|
|
};
|
|
let current = required_ref(cache, &format!("refs/remotes/origin/{branch}"))?;
|
|
if previous == current || is_ancestor(cache, previous, ¤t)? {
|
|
return Ok(());
|
|
}
|
|
if cache_has_previous {
|
|
return Err("PNCC_REMOTE_HISTORY_REWRITE_REJECTED".into());
|
|
}
|
|
let mut depth = 2;
|
|
while depth <= MAX_CONTINUITY_DEPTH {
|
|
fetch_depth(cache, &refspec, depth)?;
|
|
if is_ancestor(cache, previous, ¤t)? {
|
|
return Ok(());
|
|
}
|
|
depth = depth.saturating_mul(2);
|
|
}
|
|
Err("PNCC_REMOTE_CONTINUITY_WINDOW_EXCEEDED".into())
|
|
}
|
|
|
|
fn fetch_depth(cache: &Path, refspec: &str, depth: usize) -> Result<(), String> {
|
|
let depth = format!("--depth={depth}");
|
|
let output = git_at(cache)
|
|
.args([
|
|
"fetch",
|
|
"--no-tags",
|
|
&depth,
|
|
"--filter=blob:none",
|
|
"origin",
|
|
refspec,
|
|
])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_REMOTE_INCREMENTAL_FETCH_FAILED: {error}"))?;
|
|
require_partial_fetch(output)
|
|
}
|
|
|
|
fn fetch_incremental(cache: &Path, refspec: &str) -> Result<(), String> {
|
|
let output = git_at(cache)
|
|
.args([
|
|
"fetch",
|
|
"--no-tags",
|
|
"--update-shallow",
|
|
"--filter=blob:none",
|
|
"origin",
|
|
refspec,
|
|
])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_REMOTE_INCREMENTAL_FETCH_FAILED: {error}"))?;
|
|
require_partial_fetch(output)
|
|
}
|
|
|
|
fn require_partial_fetch(output: Output) -> Result<(), String> {
|
|
let output = require_success(output, "PNCC_REMOTE_INCREMENTAL_FETCH")?;
|
|
let diagnostic = String::from_utf8_lossy(&output.stderr);
|
|
if diagnostic.contains("filtering not recognized") || diagnostic.contains("filter-spec") {
|
|
Err("PNCC_REMOTE_PARTIAL_OBJECT_PROTOCOL_REQUIRED".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn read_regular_blob(
|
|
cache: &Path,
|
|
head: &str,
|
|
relative: &str,
|
|
maximum: usize,
|
|
) -> Result<(String, Vec<u8>), String> {
|
|
validate_head(head)?;
|
|
validate_relative_path(relative)?;
|
|
let tree = git_text(
|
|
cache,
|
|
&["ls-tree", head, "--", relative],
|
|
"PNCC_REMOTE_OBJECT_MODE_READ",
|
|
)?;
|
|
let mut fields = tree.split_whitespace();
|
|
let mode = fields.next().unwrap_or_default();
|
|
let kind = fields.next().unwrap_or_default();
|
|
let object_id = fields.next().unwrap_or_default().to_ascii_lowercase();
|
|
if !matches!(mode, "100644" | "100755") || kind != "blob" {
|
|
return Err("PNCC_REMOTE_OBJECT_NOT_REGULAR_FILE".into());
|
|
}
|
|
validate_head(&object_id)?;
|
|
let bytes = git_bytes(
|
|
cache,
|
|
&["cat-file", "blob", &object_id],
|
|
"PNCC_REMOTE_OBJECT_READ",
|
|
)?;
|
|
if bytes.is_empty() || bytes.len() > maximum {
|
|
return Err("PNCC_REMOTE_OBJECT_SIZE_INVALID".into());
|
|
}
|
|
Ok((object_id, bytes))
|
|
}
|
|
|
|
fn is_ancestor(cache: &Path, ancestor: &str, descendant: &str) -> Result<bool, String> {
|
|
validate_head(ancestor)?;
|
|
validate_head(descendant)?;
|
|
let output = git_at(cache)
|
|
.args(["merge-base", "--is-ancestor", ancestor, descendant])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_REMOTE_CONTINUITY_CHECK_FAILED: {error}"))?;
|
|
match output.status.code() {
|
|
Some(0) => Ok(true),
|
|
Some(1) => Ok(false),
|
|
_ => Err(format!(
|
|
"PNCC_REMOTE_CONTINUITY_CHECK_FAILED: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn optional_ref(cache: &Path, reference: &str) -> Result<Option<String>, String> {
|
|
let output = git_at(cache)
|
|
.args(["rev-parse", "--verify", reference])
|
|
.output()
|
|
.map_err(|error| format!("PNCC_REMOTE_CURSOR_READ_FAILED: {error}"))?;
|
|
if !output.status.success() {
|
|
return Ok(None);
|
|
}
|
|
let value = String::from_utf8(output.stdout)
|
|
.map_err(|_| "PNCC_REMOTE_CURSOR_NOT_UTF8".to_string())?
|
|
.trim()
|
|
.to_ascii_lowercase();
|
|
validate_head(&value).map(Some)
|
|
}
|
|
|
|
fn required_ref(cache: &Path, reference: &str) -> Result<String, String> {
|
|
optional_ref(cache, reference)?.ok_or_else(|| "PNCC_REMOTE_FETCH_HEAD_MISSING".into())
|
|
}
|
|
|
|
fn read_cursor(path: &Path, identity: &str, branch: &str) -> Result<Option<CursorRecord>, String> {
|
|
if !path.exists() {
|
|
return Ok(None);
|
|
}
|
|
let record: CursorRecord = read_json(path)?;
|
|
if record.schema != CURSOR_SCHEMA
|
|
|| record.remote_identity_hash != identity
|
|
|| record.branch != branch
|
|
|| record.generation == 0
|
|
{
|
|
return Err("PNCC_REMOTE_CURSOR_RECORD_INVALID".into());
|
|
}
|
|
validate_head(&record.verified_head)?;
|
|
if let Some(previous) = record.previous_verified_head.as_deref() {
|
|
validate_head(previous)?;
|
|
}
|
|
Ok(Some(record))
|
|
}
|
|
|
|
fn child_root(root: &Path, child: &str) -> Result<PathBuf, String> {
|
|
let canonical_root = root
|
|
.canonicalize()
|
|
.map_err(|error| format!("PNCC_REMOTE_STORAGE_UNAVAILABLE: {error}"))?;
|
|
let path = root.join(child);
|
|
fs::create_dir_all(&path)
|
|
.map_err(|error| format!("PNCC_REMOTE_STORAGE_UNAVAILABLE: {error}"))?;
|
|
let canonical = path
|
|
.canonicalize()
|
|
.map_err(|error| format!("PNCC_REMOTE_STORAGE_UNAVAILABLE: {error}"))?;
|
|
if canonical.parent() != Some(canonical_root.as_path()) || !canonical.is_dir() {
|
|
return Err("PNCC_REMOTE_STORAGE_BOUNDARY_INVALID".into());
|
|
}
|
|
Ok(canonical)
|
|
}
|
|
|
|
fn lock_file(path: &Path) -> Result<std::fs::File, String> {
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(false)
|
|
.open(path)
|
|
.map_err(|error| format!("PNCC_REMOTE_LOCK_FAILED: {error}"))?;
|
|
file.lock_exclusive()
|
|
.map_err(|error| format!("PNCC_REMOTE_LOCK_FAILED: {error}"))?;
|
|
Ok(file)
|
|
}
|
|
|
|
fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), String> {
|
|
let parent = path
|
|
.parent()
|
|
.ok_or("PNCC_REMOTE_STORAGE_BOUNDARY_INVALID")?;
|
|
let temporary = parent.join(format!(".pncc-{}.tmp", Uuid::new_v4()));
|
|
let bytes = serde_json::to_vec_pretty(value)
|
|
.map_err(|error| format!("PNCC_REMOTE_RECORD_INVALID: {error}"))?;
|
|
let mut file = OpenOptions::new()
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(&temporary)
|
|
.map_err(|error| format!("PNCC_REMOTE_RECORD_WRITE_FAILED: {error}"))?;
|
|
file.write_all(&bytes)
|
|
.and_then(|_| file.sync_all())
|
|
.map_err(|error| format!("PNCC_REMOTE_RECORD_WRITE_FAILED: {error}"))?;
|
|
fs::rename(&temporary, path)
|
|
.map_err(|error| format!("PNCC_REMOTE_RECORD_WRITE_FAILED: {error}"))
|
|
}
|
|
|
|
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
|
|
let metadata = fs::symlink_metadata(path).map_err(|error| {
|
|
if error.kind() == std::io::ErrorKind::NotFound {
|
|
"PNCC_REMOTE_RECORD_NOT_FOUND".to_string()
|
|
} else {
|
|
format!("PNCC_REMOTE_RECORD_READ_FAILED: {error}")
|
|
}
|
|
})?;
|
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
|
return Err("PNCC_REMOTE_RECORD_INVALID".into());
|
|
}
|
|
serde_json::from_slice(
|
|
&fs::read(path).map_err(|error| format!("PNCC_REMOTE_RECORD_READ_FAILED: {error}"))?,
|
|
)
|
|
.map_err(|error| format!("PNCC_REMOTE_RECORD_INVALID: {error}"))
|
|
}
|
|
|
|
fn directory_size(root: &Path) -> Result<u64, String> {
|
|
let mut total = 0_u64;
|
|
let mut pending = vec![root.to_path_buf()];
|
|
while let Some(path) = pending.pop() {
|
|
let metadata = fs::symlink_metadata(&path)
|
|
.map_err(|error| format!("PNCC_REMOTE_CACHE_MEASURE_FAILED: {error}"))?;
|
|
if metadata.file_type().is_symlink() {
|
|
return Err("PNCC_REMOTE_CACHE_SYMLINK_REJECTED".into());
|
|
}
|
|
if metadata.is_file() {
|
|
total = total.saturating_add(metadata.len());
|
|
} else if metadata.is_dir() {
|
|
for entry in fs::read_dir(&path)
|
|
.map_err(|error| format!("PNCC_REMOTE_CACHE_MEASURE_FAILED: {error}"))?
|
|
{
|
|
pending.push(
|
|
entry
|
|
.map_err(|error| format!("PNCC_REMOTE_CACHE_MEASURE_FAILED: {error}"))?
|
|
.path(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(total)
|
|
}
|
|
|
|
fn remove_exact_cache(root: &Path, cache: &Path) -> Result<(), String> {
|
|
if cache.parent() != Some(root) || cache == root || !cache.starts_with(root) {
|
|
return Err("PNCC_REMOTE_CACHE_BOUNDARY_INVALID".into());
|
|
}
|
|
fs::remove_dir_all(cache).map_err(|error| format!("PNCC_REMOTE_CACHE_EVICTION_FAILED: {error}"))
|
|
}
|
|
|
|
fn trusted_git() -> Command {
|
|
let executable = if cfg!(windows) { "git" } else { "/usr/bin/git" };
|
|
let mut command = Command::new(executable);
|
|
command.env_clear();
|
|
#[cfg(unix)]
|
|
command.env("PATH", "/usr/bin:/bin");
|
|
#[cfg(windows)]
|
|
for key in ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"] {
|
|
if let Some(value) = std::env::var_os(key) {
|
|
command.env(key, value);
|
|
}
|
|
}
|
|
command
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.env(
|
|
"GIT_CONFIG_GLOBAL",
|
|
if cfg!(windows) { "NUL" } else { "/dev/null" },
|
|
)
|
|
.env("GIT_TERMINAL_PROMPT", "0")
|
|
.env("GCM_INTERACTIVE", "Never")
|
|
.args(["-c", "protocol.file.allow=never"]);
|
|
if cfg!(test) {
|
|
command.args(["-c", "protocol.file.allow=always"]);
|
|
}
|
|
command
|
|
}
|
|
|
|
fn git_at(path: &Path) -> Command {
|
|
let mut command = trusted_git();
|
|
command.arg("-C").arg(path);
|
|
command
|
|
}
|
|
|
|
fn git_status(command: &mut Command, label: &str) -> Result<(), String> {
|
|
let output = command
|
|
.output()
|
|
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
|
require_success(output, label).map(|_| ())
|
|
}
|
|
|
|
fn git_text(path: &Path, args: &[&str], label: &str) -> Result<String, String> {
|
|
let output = git_at(path)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
|
String::from_utf8(require_success(output, label)?.stdout)
|
|
.map_err(|_| format!("{label}_NOT_UTF8"))
|
|
}
|
|
|
|
fn git_bytes(path: &Path, args: &[&str], label: &str) -> Result<Vec<u8>, String> {
|
|
let output = git_at(path)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
|
Ok(require_success(output, label)?.stdout)
|
|
}
|
|
|
|
fn require_success(output: Output, label: &str) -> Result<Output, String> {
|
|
if output.status.success() {
|
|
Ok(output)
|
|
} else {
|
|
Err(format!(
|
|
"{label}_FAILED: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
))
|
|
}
|
|
}
|
|
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
|
digest(&SHA256, bytes)
|
|
.as_ref()
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
fn fixture() -> (TempDir, PathBuf, PathBuf, String) {
|
|
let temp = TempDir::new().unwrap();
|
|
let source = temp.path().join("source");
|
|
let remote = temp.path().join("remote.git");
|
|
let storage = temp.path().join("storage");
|
|
fs::create_dir_all(source.join("brain")).unwrap();
|
|
fs::create_dir(&storage).unwrap();
|
|
test_git(&source, &["init", "-b", "main"]);
|
|
fs::write(source.join("brain/CORE.hldp"), "first\n").unwrap();
|
|
test_git(&source, &["add", "."]);
|
|
test_git(
|
|
&source,
|
|
&[
|
|
"-c",
|
|
"user.name=PNCC Test",
|
|
"-c",
|
|
"user.email=pncc@test.invalid",
|
|
"commit",
|
|
"-m",
|
|
"first",
|
|
],
|
|
);
|
|
let output = trusted_git()
|
|
.args(["init", "--bare", &remote.to_string_lossy()])
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success());
|
|
test_git(&remote, &["config", "uploadpack.allowFilter", "true"]);
|
|
test_git(
|
|
&remote,
|
|
&["config", "uploadpack.allowAnySHA1InWant", "true"],
|
|
);
|
|
test_git(
|
|
&source,
|
|
&["remote", "add", "origin", &remote.to_string_lossy()],
|
|
);
|
|
test_git(&source, &["push", "-u", "origin", "main"]);
|
|
let remote_url = format!("file://{}", remote.display());
|
|
register_remote_mount_at(
|
|
&storage,
|
|
RegisterPnccRemoteMountInput {
|
|
mount_id: "persona-primary".into(),
|
|
remote_url: remote_url.clone(),
|
|
branch: None,
|
|
},
|
|
true,
|
|
)
|
|
.unwrap();
|
|
(temp, source, storage, remote_url)
|
|
}
|
|
|
|
fn test_git(path: &Path, args: &[&str]) -> String {
|
|
let mut command = trusted_git();
|
|
command
|
|
.env("GIT_ALLOW_PROTOCOL", "file")
|
|
.arg("-C")
|
|
.arg(path)
|
|
.args(args);
|
|
let output = command.output().unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"git {args:?}: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
String::from_utf8_lossy(&output.stdout).trim().into()
|
|
}
|
|
|
|
fn read_input() -> ReadMountedPnccRemoteObjectInput {
|
|
ReadMountedPnccRemoteObjectInput {
|
|
mount_id: "persona-primary".into(),
|
|
relative_path: "brain/CORE.hldp".into(),
|
|
expected_head: None,
|
|
max_cache_bytes: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reads_incrementally_without_worktree_or_model_execution() {
|
|
let (_temp, _source, storage, _url) = fixture();
|
|
let first =
|
|
read_mounted_remote_object_with_file_policy(&storage, read_input(), true).unwrap();
|
|
assert_eq!(first.content, "first\n");
|
|
assert!(first.cursor_advanced);
|
|
assert!(first.continuity_verified);
|
|
assert!(first.fetched_incremental_objects);
|
|
assert!(!first.worktree_created);
|
|
assert!(!first.full_history_requested);
|
|
assert!(!first.model_inference_started);
|
|
assert!(!first.reality_execution_allowed);
|
|
let second =
|
|
read_mounted_remote_object_with_file_policy(&storage, read_input(), true).unwrap();
|
|
assert!(!second.cursor_advanced);
|
|
assert!(!second.fetched_incremental_objects);
|
|
assert_eq!(second.previous_verified_head, Some(first.remote_head));
|
|
}
|
|
|
|
#[test]
|
|
fn appends_only_a_descendant_segment_and_rejects_rewritten_history() {
|
|
let (_temp, source, storage, _url) = fixture();
|
|
let first =
|
|
read_mounted_remote_object_with_file_policy(&storage, read_input(), true).unwrap();
|
|
fs::write(source.join("brain/CORE.hldp"), "second\n").unwrap();
|
|
test_git(&source, &["add", "."]);
|
|
test_git(
|
|
&source,
|
|
&[
|
|
"-c",
|
|
"user.name=PNCC Test",
|
|
"-c",
|
|
"user.email=pncc@test.invalid",
|
|
"commit",
|
|
"-m",
|
|
"second",
|
|
],
|
|
);
|
|
test_git(&source, &["push", "origin", "main"]);
|
|
let second =
|
|
read_mounted_remote_object_with_file_policy(&storage, read_input(), true).unwrap();
|
|
assert_eq!(second.previous_verified_head, Some(first.remote_head));
|
|
assert_eq!(second.content, "second\n");
|
|
|
|
test_git(&source, &["checkout", "--orphan", "rewrite"]);
|
|
test_git(&source, &["rm", "-rf", "."]);
|
|
fs::create_dir_all(source.join("brain")).unwrap();
|
|
fs::write(source.join("brain/CORE.hldp"), "rewrite\n").unwrap();
|
|
test_git(&source, &["add", "."]);
|
|
test_git(
|
|
&source,
|
|
&[
|
|
"-c",
|
|
"user.name=PNCC Test",
|
|
"-c",
|
|
"user.email=pncc@test.invalid",
|
|
"commit",
|
|
"-m",
|
|
"rewrite",
|
|
],
|
|
);
|
|
test_git(&source, &["branch", "-M", "main"]);
|
|
test_git(&source, &["push", "--force", "origin", "main"]);
|
|
assert_eq!(
|
|
read_mounted_remote_object_with_file_policy(&storage, read_input(), true).unwrap_err(),
|
|
"PNCC_REMOTE_HISTORY_REWRITE_REJECTED"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn public_registration_rejects_credentials_and_non_https_remotes() {
|
|
assert_eq!(
|
|
validate_remote_url("https://token@example.invalid/repo.git", false).unwrap_err(),
|
|
"PNCC_REMOTE_URL_MUST_BE_CREDENTIAL_FREE_HTTPS"
|
|
);
|
|
assert_eq!(
|
|
validate_remote_url("http://example.invalid/repo.git", false).unwrap_err(),
|
|
"PNCC_REMOTE_URL_MUST_BE_CREDENTIAL_FREE_HTTPS"
|
|
);
|
|
assert_eq!(
|
|
validate_relative_path("../brain.hldp").unwrap_err(),
|
|
"PNCC_REMOTE_PATH_INVALID"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_eviction_keeps_the_durable_continuity_cursor() {
|
|
let (_temp, _source, storage, _url) = fixture();
|
|
let mut input = read_input();
|
|
input.max_cache_bytes = Some(MIN_CACHE_LIMIT_BYTES);
|
|
let objects = child_root(&storage, "objects").unwrap();
|
|
let first =
|
|
read_mounted_remote_object_with_file_policy(&storage, input.clone(), true).unwrap();
|
|
let cache = objects.join(format!("{}.git", first.remote_identity_hash));
|
|
fs::write(
|
|
cache.join("force-budget-overrun"),
|
|
vec![0_u8; MIN_CACHE_LIMIT_BYTES as usize],
|
|
)
|
|
.unwrap();
|
|
let second = read_mounted_remote_object_with_file_policy(&storage, input, true).unwrap();
|
|
assert_eq!(second.cache_state, "EVICTED_AFTER_BOUNDED_READ");
|
|
assert!(!cache.exists());
|
|
assert!(storage
|
|
.join("continuity")
|
|
.join(format!("{}.json", second.remote_identity_hash))
|
|
.is_file());
|
|
}
|
|
}
|