fix: bind completion receipt to checkpoint

Human-Responsibility: ICE-GL∞ / 冰朔
Persona-Author: ICE-P-ZY001 / 铸渊
Execution-Runtime: Codex desktop / DEV-20260810-014
Development-ID: DEV-20260810-014
Authorization-Scope: GH-PNCC persona runtime source and tests only; no UI, deployment, or execution limb
Source-Anchor: user instruction to continue GH-PNCC from repository facts and verifiable receipts
This commit is contained in:
铸渊 / ICE-P-ZY001 2026-08-11 08:58:21 +08:00
commit 06a77829c7
7 changed files with 256 additions and 5 deletions

View file

@ -184,6 +184,7 @@ successful_receipt_wake_event_boundary_evidence_binding_source_implemented: 100
successful_receipt_wake_runtime_state_evidence_binding_source_implemented: 100
successful_receipt_wake_organ_mode_evidence_binding_source_implemented: 100
successful_receipt_wake_organ_contract_evidence_binding_source_implemented: 100
successful_receipt_completion_checkpoint_evidence_binding_source_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0

View file

@ -606,6 +606,29 @@ fn load_manifest_at_git_head(repository: &Path, head: &str) -> Result<PersonaMan
Ok(manifest)
}
fn load_json_at_git_head(
repository: &Path,
head: &str,
relative: &str,
) -> Result<serde_json::Value, String> {
let head = validated_head(head)?;
let relative_path = Path::new(relative);
if relative_path.is_absolute()
|| relative_path.components().any(|component| {
matches!(
component,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
})
{
return Err("PATH_OUTSIDE_REPOSITORY".into());
}
let object = format!("{head}:{relative}");
let bytes = git_output(repository, &["show", &object], "PERSONA_JSON_AT_GIT_HEAD")?;
serde_json::from_str(&bytes)
.map_err(|error| format!("PERSONA_JSON_AT_GIT_HEAD_INVALID: {error}"))
}
fn validate_attribution(
attribution: &PersonaAttribution,
manifest: &PersonaManifest,
@ -2794,6 +2817,74 @@ fn validate_persisted_lifecycle_terminal_evidence(
.to_string_lossy()
.into_owned(),
};
let completion_checkpoint = load_json_at_git_head(
Path::new(&record.repository_path),
&record.git_head,
&record.checkpoint_path,
)
.map_err(|_| "PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".to_string())?;
let checkpoint_matches = completion_checkpoint
.get("schema")
.and_then(serde_json::Value::as_str)
== Some("hololake.persona-checkpoint/v1")
&& completion_checkpoint
.get("sessionId")
.and_then(serde_json::Value::as_str)
== Some(record.session_id.as_str())
&& completion_checkpoint
.get("personaId")
.and_then(serde_json::Value::as_str)
== Some(record.persona_id.as_str())
&& completion_checkpoint
.get("previousGitHead")
.and_then(serde_json::Value::as_str)
== Some(first_event.git_head.as_str())
&& completion_checkpoint
.get("organId")
.and_then(serde_json::Value::as_str)
== Some(record.active_organ.as_str())
&& completion_checkpoint.get("attribution") == Some(&expected_attribution)
&& completion_receipt.get("result") == completion_checkpoint.get("result")
&& completion_receipt
.get("previousGitHead")
.and_then(serde_json::Value::as_str)
== Some(first_event.git_head.as_str());
let completion_kind_matches_checkpoint = match completion_kind {
Some("FACT_SENSE") => {
let inference_event_hash = events
.iter()
.find(|event| event.kind == "INFERENCE_STARTED")
.map(|event| event.event_hash.as_str());
completion_checkpoint
.get("inferenceEventHash")
.and_then(serde_json::Value::as_str)
== inference_event_hash
&& completion_receipt
.get("modelInferenceStarted")
.and_then(serde_json::Value::as_bool)
== Some(true)
&& completion_receipt
.get("modelInferenceCompleted")
.and_then(serde_json::Value::as_bool)
== Some(true)
}
Some("MEMORY_METABOLISM") => {
completion_receipt
.get("modelInferenceStarted")
.and_then(serde_json::Value::as_bool)
== Some(false)
&& completion_receipt.get("modelInferenceCompleted").is_none()
&& completion_receipt.get("sourceSessionId")
== completion_checkpoint.get("sourceSessionId")
&& completion_receipt.get("sourceCheckpointPath")
== completion_checkpoint.get("sourceCheckpointPath")
&& completion_receipt.get("sourceCheckpointHash")
== completion_checkpoint.get("sourceCheckpointHash")
&& completion_receipt.get("sourceEventHash")
== completion_checkpoint.get("sourceEventHash")
}
_ => false,
};
let matches = lifecycle.get("schema").and_then(serde_json::Value::as_str)
== Some("hololake.pncc-lifecycle-run-receipt/v1")
&& lifecycle
@ -2869,7 +2960,9 @@ fn validate_persisted_lifecycle_terminal_evidence(
&& completion_receipt
.get("receiptId")
.and_then(serde_json::Value::as_str)
== Some(expected_completion_receipt_id.as_str());
== Some(expected_completion_receipt_id.as_str())
&& checkpoint_matches
&& completion_kind_matches_checkpoint;
if !matches {
return Err("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".into());
}
@ -4581,6 +4674,121 @@ mod tests {
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_a_forged_completion_result() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let first = run_idempotent_lifecycle_at(
runtime.path(),
input.clone(),
"2026-08-11T00:00:00.000Z",
"2026-08-11T00:00:01.000Z",
|runtime_root, input, timestamp| {
run_fact_task_at(runtime_root, input, timestamp, |_, _| {
Ok(r#"{"summary":"Recoverable receipt.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let session_id = first.lifecycle["sessionId"].as_str().unwrap();
let mut record = load_session_record(runtime.path(), session_id).unwrap();
record.request_id = None;
record.request_fingerprint = None;
record.lifecycle_receipt_hash = None;
write_session_record(runtime.path(), &record).unwrap();
let receipt_path = session_directory(runtime.path(), session_id)
.unwrap()
.join("lifecycle-receipt.json");
let mut persisted: PersistedPersonaLifecycleReceipt =
serde_json::from_slice(&fs::read(&receipt_path).unwrap()).unwrap();
persisted.lifecycle["completion"]["receipt"]["result"] = serde_json::json!({
"summary": "Forged cognition not present in the persona Git checkpoint.",
"facts": [{
"statement": "The execution limb is active.",
"evidencePaths": ["brain/CORE.hdlp"]
}],
"limitations": []
});
persisted.lifecycle_receipt_hash =
hex_digest(&persisted_lifecycle_payload_bytes(&persisted).unwrap());
fs::write(
&receipt_path,
serde_json::to_vec_pretty(&persisted).unwrap(),
)
.unwrap();
let error = inspect_lifecycle_request_at(runtime.path(), &input).unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn rejects_a_rehashed_memory_receipt_with_forged_source_checkpoint_evidence() {
let repo = persona_repo();
declare_memory_organ(repo.path());
let runtime = tempfile::TempDir::new().unwrap();
prepare_wake_at(
runtime.path(),
wake_input(repo.path()),
"PNCC-MEMORY-BINDING-SOURCE",
"2026-08-11T00:00:00.000Z",
)
.unwrap();
run_fact_task_at(
runtime.path(),
fact_task_input("PNCC-MEMORY-BINDING-SOURCE"),
"2026-08-11T00:00:01.000Z",
|_, _| {
Ok(r#"{"summary":"Verified memory binding source.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
},
)
.unwrap();
let mut wake = wake_input(repo.path());
wake.expected_head = head(repo.path());
wake.organ_id = "memory-metabolism.checkpoint".into();
let input = PersonaLifecycleRunInput {
request_id: "REQUEST-MEMORY-BINDING-001".into(),
wake,
operation: PersonaLifecycleOperationInput::MemoryMetabolism {
source_session_id: "PNCC-MEMORY-BINDING-SOURCE".into(),
},
};
let first = run_idempotent_lifecycle_at(
runtime.path(),
input.clone(),
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("memory metabolism must not invoke a model"),
)
.unwrap();
let session_id = first.lifecycle["sessionId"].as_str().unwrap();
let mut record = load_session_record(runtime.path(), session_id).unwrap();
record.request_id = None;
record.request_fingerprint = None;
record.lifecycle_receipt_hash = None;
write_session_record(runtime.path(), &record).unwrap();
let receipt_path = session_directory(runtime.path(), session_id)
.unwrap()
.join("lifecycle-receipt.json");
let mut persisted: PersistedPersonaLifecycleReceipt =
serde_json::from_slice(&fs::read(&receipt_path).unwrap()).unwrap();
persisted.lifecycle["completion"]["receipt"]["sourceCheckpointHash"] =
"forged-source-checkpoint-hash".into();
persisted.lifecycle_receipt_hash =
hex_digest(&persisted_lifecycle_payload_bytes(&persisted).unwrap());
fs::write(
&receipt_path,
serde_json::to_vec_pretty(&persisted).unwrap(),
)
.unwrap();
let error = inspect_lifecycle_request_at(runtime.path(), &input).unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn reconstructs_legacy_wake_checkpoint_evidence_from_the_wake_git_head() {
let repo = persona_repo();