fix: bind successful receipt wake 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 07:08:16 +08:00
commit 869611962e
7 changed files with 175 additions and 4 deletions

View file

@ -431,6 +431,8 @@ struct PersonaSessionRecord {
repository_path: String,
git_head: String,
brain_entry: String,
#[serde(default)]
wake_checkpoint_path: Option<String>,
checkpoint_path: String,
node_id: String,
model_provider_id: String,
@ -587,6 +589,22 @@ fn load_manifest(repository: &Path) -> Result<PersonaManifest, String> {
Ok(manifest)
}
fn load_manifest_at_git_head(repository: &Path, head: &str) -> Result<PersonaManifest, String> {
let head = validated_head(head)?;
let object = format!("{head}:{MANIFEST_PATH}");
let bytes = git_output(
repository,
&["show", &object],
"PERSONA_MANIFEST_AT_GIT_HEAD",
)?;
let manifest: PersonaManifest = serde_json::from_str(&bytes)
.map_err(|error| format!("PERSONA_MANIFEST_AT_GIT_HEAD_INVALID: {error}"))?;
if manifest.schema != "hololake.persona/v1" {
return Err("PERSONA_MANIFEST_SCHEMA_UNSUPPORTED".into());
}
Ok(manifest)
}
fn validate_attribution(
attribution: &PersonaAttribution,
manifest: &PersonaManifest,
@ -1294,6 +1312,7 @@ fn prepare_wake_at(
repository_path: repository.to_string_lossy().into_owned(),
git_head: git_head.clone(),
brain_entry: manifest.brain_entry.clone(),
wake_checkpoint_path: Some(checkpoint.to_string_lossy().into_owned()),
checkpoint_path: manifest.current_checkpoint.clone(),
node_id: node_id.clone(),
model_provider_id,
@ -2740,6 +2759,26 @@ fn validate_persisted_lifecycle_terminal_evidence(
repository_file(Path::new(&record.repository_path), &record.brain_entry)
.map_err(|_| "PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".to_string())?;
let expected_brain_entry = expected_brain_entry.to_string_lossy();
let expected_wake_checkpoint = match record.wake_checkpoint_path.as_ref() {
Some(path) => path.clone(),
None => {
let wake_manifest = load_manifest_at_git_head(
Path::new(&record.repository_path),
&first_event.git_head,
)
.map_err(|_| "PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".to_string())?;
if wake_manifest.persona_id != record.persona_id {
return Err("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".into());
}
repository_file(
Path::new(&record.repository_path),
&wake_manifest.current_checkpoint,
)
.map_err(|_| "PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".to_string())?
.to_string_lossy()
.into_owned()
}
};
let matches = lifecycle.get("schema").and_then(serde_json::Value::as_str)
== Some("hololake.pncc-lifecycle-run-receipt/v1")
&& lifecycle
@ -2760,6 +2799,10 @@ fn validate_persisted_lifecycle_terminal_evidence(
== Some(first_event.git_head.as_str())
&& wake.get("brainEntry").and_then(serde_json::Value::as_str)
== Some(expected_brain_entry.as_ref())
&& wake
.get("checkpointPath")
.and_then(serde_json::Value::as_str)
== Some(expected_wake_checkpoint.as_str())
&& wake.get("nodeId").and_then(serde_json::Value::as_str) == Some(record.node_id.as_str())
&& wake
.get("modelInstanceId")
@ -4293,6 +4336,89 @@ mod tests {
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_a_forged_wake_checkpoint_path() {
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["wakeReceipt"]["checkpointPath"] = "/forged/CURRENT.hdlp".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();
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 session_path = session_directory(runtime.path(), session_id)
.unwrap()
.join("session.json");
let mut legacy_record: serde_json::Value =
serde_json::from_slice(&fs::read(&session_path).unwrap()).unwrap();
legacy_record
.as_object_mut()
.unwrap()
.remove("wakeCheckpointPath");
legacy_record["requestId"] = serde_json::Value::Null;
legacy_record["requestFingerprint"] = serde_json::Value::Null;
legacy_record["lifecycleReceiptHash"] = serde_json::Value::Null;
fs::write(
&session_path,
serde_json::to_vec_pretty(&legacy_record).unwrap(),
)
.unwrap();
let inspection = inspect_lifecycle_request_at(runtime.path(), &input).unwrap();
assert_eq!(inspection.status, "SAFE_BIND_PERSISTED_RECEIPT");
assert!(inspection.safe_to_bind_receipt);
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_a_malformed_wake_event_hash() {
let repo = persona_repo();