fix: bind wake receipt to event journal

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 09:20:55 +08:00
commit 1cd8a1b489
7 changed files with 103 additions and 9 deletions

View file

@ -185,6 +185,7 @@ 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
successful_receipt_event_journal_evidence_binding_source_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -218,6 +219,10 @@ Git 提交、检查点、回执编号和完整事件链重新绑定到已验证
伪造另一个仓库、人格作者或提交结果;任何语义证据不一致都以
`PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH` 失败关闭。
成功唤醒回执中的事件日志路径也不能只是载荷自述。检查、恢复绑定和重放会从受信任的运行时根与
已验证会话编号重建唯一 `sessions/<session-id>/events.jsonl`,再与 `eventJournal` 精确比对。同步改写
回执和外层哈希不能把人格运行轨迹指向另一份日志或另一会话。
成功回执里的大脑入口也不能只是载荷内的自述。检查、恢复绑定和重放会从已验证会话记录中的
人格仓库与相对大脑入口重新解析规范文件路径,再与唤醒回执的 `brainEntry` 比对。即使攻击者
同步改写成功回执并重算载荷哈希,也不能把一次真实唤醒伪装成启动了另一个大脑文件。

View file

@ -2708,6 +2708,7 @@ fn persisted_lifecycle_identity_matches(
}
fn validate_persisted_lifecycle_terminal_evidence(
runtime_root: &Path,
persisted: &PersistedPersonaLifecycleReceipt,
record: &PersonaSessionRecord,
events: &[PersonaLifecycleEvent],
@ -2807,6 +2808,10 @@ 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_event_journal = session_directory(runtime_root, &record.session_id)?
.join("events.jsonl")
.to_string_lossy()
.into_owned();
let expected_wake_checkpoint = match record.wake_checkpoint_path.as_ref() {
Some(path) => path.clone(),
None => repository_file(
@ -2924,6 +2929,8 @@ fn validate_persisted_lifecycle_terminal_evidence(
== Some(record.active_organ.as_str())
&& wake.get("organMode").and_then(serde_json::Value::as_str) == Some(expected_organ_mode)
&& wake.get("organContract") == Some(&expected_organ_contract)
&& wake.get("eventJournal").and_then(serde_json::Value::as_str)
== Some(expected_event_journal.as_str())
&& wake.get("attribution") == Some(&expected_attribution)
&& wake.get("receiptId").and_then(serde_json::Value::as_str)
== Some(expected_wake_receipt_id.as_str())
@ -3043,7 +3050,7 @@ fn verified_lifecycle_replay(
{
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
validate_persisted_lifecycle_terminal_evidence(runtime_root, &persisted, &record, &events)?;
if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
@ -3130,7 +3137,7 @@ fn inspect_lifecycle_request_at(
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
let terminal_event = events.last().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?;
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
validate_persisted_lifecycle_terminal_evidence(runtime_root, &persisted, &record, &events)?;
let complete = record.request_id.as_deref() == Some(request_id.as_str())
&& record.request_fingerprint.as_deref() == Some(request_fingerprint.as_str())
&& record.lifecycle_receipt_hash.as_deref()
@ -3236,7 +3243,7 @@ fn bind_persisted_lifecycle_receipt_at(
}
let events = verify_event_journal(runtime_root, &record)?;
let terminal_event = events.last().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?;
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
validate_persisted_lifecycle_terminal_evidence(runtime_root, &persisted, &record, &events)?;
let expected_state = if persisted.outcome == "FAILED" {
"DORMANT_AFTER_FAILURE"
} else {
@ -3384,7 +3391,7 @@ fn persist_successful_lifecycle_receipt(
if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
validate_persisted_lifecycle_terminal_evidence(runtime_root, &persisted, &record, &events)?;
// Persist the immutable full receipt before binding it into the mutable session record. A
// crash between these atomic writes leaves a verifiable, explicitly recoverable state.
write_json_file(
@ -4494,6 +4501,49 @@ mod tests {
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_a_forged_event_journal_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"]["eventJournal"] =
"/forged/runtime/sessions/other/events.jsonl".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 rejects_a_rehashed_completed_receipt_with_truncated_wake_events() {
let repo = persona_repo();