fix: bind successful lifecycle semantic evidence

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 06:11:19 +08:00
commit a6504ca749
6 changed files with 370 additions and 19 deletions

View file

@ -177,6 +177,7 @@ idempotent_replay_repository_state_revalidation_source_implemented: 100
successful_completion_receipt_terminal_revalidation_source_implemented: 100
safe_receipt_binding_terminal_revalidation_source_implemented: 100
failure_receipt_terminal_evidence_binding_source_implemented: 100
successful_receipt_semantic_evidence_binding_source_implemented: 100
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -204,6 +205,12 @@ runtime_health: 0
事件链。即使有人同步重算回执哈希,伪造终态事件或人格作者也会失败关闭,且不会把请求字段
写入会话记录。
成功回执同样不能把“载荷哈希一致”当成语义真实。首次落盘、检查、恢复绑定和重放现在会把
生命周期 schema、规范仓库、唤醒身份、节点、模型实例、器官、完整双层归因、完成类型、最终
Git 提交、检查点、回执编号和完整事件链重新绑定到已验证的会话记录。同步改写载荷与哈希不能
伪造另一个仓库、人格作者或提交结果;任何语义证据不一致都以
`PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH` 失败关闭。
成功回执与失败回执的每次重放都会重新读取规范仓库当前提交并检查工作树,而不是只信任回执
生成时的状态。当前提交偏离会话记录或工作树变脏时,检查结果降级为人工复核,重放失败关闭,
且不会重新启动器官。

View file

@ -2667,21 +2667,139 @@ fn persisted_lifecycle_identity_matches(
fn validate_persisted_lifecycle_terminal_evidence(
persisted: &PersistedPersonaLifecycleReceipt,
record: &PersonaSessionRecord,
terminal_event: &PersonaLifecycleEvent,
events: &[PersonaLifecycleEvent],
) -> Result<(), String> {
if persisted.outcome != "FAILED" {
let terminal_event = events.last().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?;
if persisted.outcome == "FAILED" {
let failure = persisted
.failure
.as_ref()
.ok_or("PERSONA_LIFECYCLE_FAILURE_RECEIPT_MISSING")?;
if failure.repository_path != record.repository_path
|| failure.terminal_event_hash != terminal_event.event_hash
|| failure.attribution != record.attribution
|| lifecycle_failure_code(&failure.error_code) != failure.error_code
{
return Err("PERSONA_LIFECYCLE_FAILURE_TERMINAL_EVIDENCE_MISMATCH".into());
}
return Ok(());
}
let failure = persisted
.failure
.as_ref()
.ok_or("PERSONA_LIFECYCLE_FAILURE_RECEIPT_MISSING")?;
if failure.repository_path != record.repository_path
|| failure.terminal_event_hash != terminal_event.event_hash
|| failure.attribution != record.attribution
|| lifecycle_failure_code(&failure.error_code) != failure.error_code
{
return Err("PERSONA_LIFECYCLE_FAILURE_TERMINAL_EVIDENCE_MISMATCH".into());
if persisted.outcome != "COMPLETED" {
return Err("PERSONA_LIFECYCLE_RECEIPT_OUTCOME_INVALID".into());
}
let lifecycle = &persisted.lifecycle;
let wake = lifecycle
.get("wakeReceipt")
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let completion = lifecycle
.get("completion")
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let completion_kind = completion.get("kind").and_then(serde_json::Value::as_str);
let completion_receipt = completion
.get("receipt")
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let expected_completion_schema = match completion_kind {
Some("FACT_SENSE") => "hololake.pncc-fact-task-receipt/v1",
Some("MEMORY_METABOLISM") => "hololake.pncc-memory-metabolism-receipt/v1",
_ => return Err("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".into()),
};
let expected_receipt_prefix = if completion_kind == Some("FACT_SENSE") {
"PNCC-TASK-"
} else {
"PNCC-MEMORY-"
};
let expected_attribution = serde_json::to_value(&record.attribution)
.map_err(|error| format!("PERSONA_LIFECYCLE_EVIDENCE_SERIALIZATION_FAILED: {error}"))?;
let expected_events = serde_json::to_value(events)
.map_err(|error| format!("PERSONA_LIFECYCLE_EVIDENCE_SERIALIZATION_FAILED: {error}"))?;
let wake_events = wake
.get("events")
.and_then(serde_json::Value::as_array)
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let expected_wake_events = expected_events
.as_array()
.and_then(|all| all.get(..wake_events.len()))
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let wake_terminal_hash = wake_events
.last()
.and_then(|event| event.get("eventHash"))
.and_then(serde_json::Value::as_str)
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let wake_receipt_suffix = wake_terminal_hash
.get(..20)
.ok_or("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH")?;
let expected_wake_receipt_id = format!("PNCC-WAKE-{wake_receipt_suffix}");
let expected_completion_receipt_id = format!(
"{}{}",
expected_receipt_prefix,
&terminal_event.event_hash[..20]
);
let first_event = events.first().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?;
let matches = lifecycle.get("schema").and_then(serde_json::Value::as_str)
== Some("hololake.pncc-lifecycle-run-receipt/v1")
&& lifecycle
.get("repositoryPath")
.and_then(serde_json::Value::as_str)
== Some(record.repository_path.as_str())
&& wake.get("schema").and_then(serde_json::Value::as_str)
== Some("hololake.pncc-wake-receipt/v1")
&& wake.get("sessionId").and_then(serde_json::Value::as_str)
== Some(record.session_id.as_str())
&& wake.get("personaId").and_then(serde_json::Value::as_str)
== Some(record.persona_id.as_str())
&& wake
.get("repositoryPath")
.and_then(serde_json::Value::as_str)
== Some(record.repository_path.as_str())
&& wake.get("gitHead").and_then(serde_json::Value::as_str)
== Some(first_event.git_head.as_str())
&& wake.get("nodeId").and_then(serde_json::Value::as_str) == Some(record.node_id.as_str())
&& wake
.get("modelInstanceId")
.and_then(serde_json::Value::as_str)
== Some(record.model_instance_id.as_str())
&& wake.get("activeOrgan").and_then(serde_json::Value::as_str)
== Some(record.active_organ.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())
&& wake_events.as_slice() == expected_wake_events
&& completion_receipt
.get("schema")
.and_then(serde_json::Value::as_str)
== Some(expected_completion_schema)
&& completion_receipt
.get("sessionId")
.and_then(serde_json::Value::as_str)
== Some(record.session_id.as_str())
&& completion_receipt
.get("personaId")
.and_then(serde_json::Value::as_str)
== Some(record.persona_id.as_str())
&& completion_receipt
.get("committedGitHead")
.and_then(serde_json::Value::as_str)
== Some(record.git_head.as_str())
&& completion_receipt
.get("checkpointPath")
.and_then(serde_json::Value::as_str)
== Some(record.checkpoint_path.as_str())
&& completion_receipt
.get("runtimeState")
.and_then(serde_json::Value::as_str)
== Some("DORMANT")
&& completion_receipt
.get("activeOrgan")
.is_some_and(serde_json::Value::is_null)
&& completion_receipt.get("attribution") == Some(&expected_attribution)
&& completion_receipt.get("events") == Some(&expected_events)
&& completion_receipt
.get("receiptId")
.and_then(serde_json::Value::as_str)
== Some(expected_completion_receipt_id.as_str());
if !matches {
return Err("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH".into());
}
Ok(())
}
@ -2760,7 +2878,7 @@ fn verified_lifecycle_replay(
{
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, terminal_event)?;
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
@ -2847,7 +2965,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, terminal_event)?;
validate_persisted_lifecycle_terminal_evidence(&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()
@ -2953,7 +3071,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, terminal_event)?;
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, &events)?;
let expected_state = if persisted.outcome == "FAILED" {
"DORMANT_AFTER_FAILURE"
} else {
@ -3101,6 +3219,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)?;
// 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(
@ -4081,6 +4200,189 @@ mod tests {
assert!(!after.safe_to_bind_receipt);
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_the_wrong_repository() {
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["repositoryPath"] = "/forged/repository".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"));
let record = load_session_record(runtime.path(), session_id).unwrap();
assert!(record.request_id.is_none());
assert!(record.lifecycle_receipt_hash.is_none());
}
#[test]
fn rejects_a_rehashed_completed_receipt_with_a_malformed_wake_event_hash() {
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"]["events"][2]["eventHash"] = "short".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 refuses_to_bind_rehashed_completed_attribution_changed_after_inspection() {
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 inspection = inspect_lifecycle_request_at(runtime.path(), &input).unwrap();
assert!(inspection.safe_to_bind_receipt);
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"]["attribution"]["personaCognitiveAuthor"] =
"FORGED-PERSONA".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 =
bind_persisted_lifecycle_receipt_at(runtime.path(), &input, &inspection).unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
let record = load_session_record(runtime.path(), session_id).unwrap();
assert!(record.request_id.is_none());
assert!(record.request_fingerprint.is_none());
assert!(record.lifecycle_receipt_hash.is_none());
}
#[test]
fn refuses_to_replay_rehashed_completed_git_evidence_bound_into_the_session() {
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":"Bound receipt.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let session_id = first.lifecycle["sessionId"].as_str().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"]["committedGitHead"] = "0".repeat(40).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 mut record = load_session_record(runtime.path(), session_id).unwrap();
record.lifecycle_receipt_hash = Some(persisted.lifecycle_receipt_hash);
write_session_record(runtime.path(), &record).unwrap();
let error = run_idempotent_lifecycle_at(
runtime.path(),
input,
"2026-08-11T00:00:02.000Z",
"2026-08-11T00:00:03.000Z",
|_, _, _| panic!("forged completion evidence must not rerun the organ"),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_EVIDENCE_MISMATCH"));
}
#[test]
fn refuses_to_bind_a_receipt_tampered_after_safe_inspection() {
let repo = persona_repo();