fix(pncc): bind failure receipts to terminal 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 05:46:12 +08:00
commit 85fc90bd52
7 changed files with 278 additions and 10 deletions

View file

@ -176,6 +176,7 @@ idempotent_terminal_failure_receipt_and_replay_source_implemented: 100
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
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -198,6 +199,11 @@ runtime_health: 0
终态、Git、租约、事件链和回执哈希全部复核后只重放原失败不再次启动模型或器官。未完成
闭合、脏仓库或仍持有租约的失败不能被伪装成终态回执。
失败回执不能只靠“载荷哈希能对上”证明语义真实。检查、恢复绑定和重放现在还会把失败回执
中的规范仓库、终态事件哈希、完整双层归因与稳定机器错误码重新绑定到当前会话记录及已验证
事件链。即使有人同步重算回执哈希,伪造终态事件或人格作者也会失败关闭,且不会把请求字段
写入会话记录。
成功回执与失败回执的每次重放都会重新读取规范仓库当前提交并检查工作树,而不是只信任回执
生成时的状态。当前提交偏离会话记录或工作树变脏时,检查结果降级为人工复核,重放失败关闭,
且不会重新启动器官。

View file

@ -82,7 +82,7 @@ struct PersonaModelBinding {
base_url: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaAttribution {
pub human_responsibility_subject: String,
@ -2664,6 +2664,28 @@ fn persisted_lifecycle_identity_matches(
}
}
fn validate_persisted_lifecycle_terminal_evidence(
persisted: &PersistedPersonaLifecycleReceipt,
record: &PersonaSessionRecord,
terminal_event: &PersonaLifecycleEvent,
) -> Result<(), String> {
if persisted.outcome != "FAILED" {
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());
}
Ok(())
}
fn lifecycle_failure_code(error: &str) -> String {
let candidate = error.split([':', ';']).next().unwrap_or_default().trim();
if !candidate.is_empty()
@ -2731,12 +2753,14 @@ fn verified_lifecycle_replay(
} else {
"DORMANT"
};
let terminal_event = events.last().ok_or("PERSONA_EVENT_CHAIN_EMPTY")?;
if record.state != expected_state
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
|| terminal_event.kind != "DORMANT"
|| primary_lease_held_by_session(runtime_root, &record)?
{
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
validate_persisted_lifecycle_terminal_evidence(&persisted, &record, terminal_event)?;
if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
@ -2822,6 +2846,8 @@ fn inspect_lifecycle_request_at(
if !persisted_lifecycle_identity_matches(&persisted, &session_id, &record.persona_id) {
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)?;
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()
@ -2838,7 +2864,7 @@ fn inspect_lifecycle_request_at(
let repository_matches_record =
observed_head == record.git_head && require_clean_repository(&repository).is_ok();
let safely_dormant = record.state == expected_state
&& events.last().map(|event| event.kind.as_str()) == Some("DORMANT")
&& terminal_event.kind == "DORMANT"
&& !lease_held
&& repository_matches_record;
let (status, safe_to_bind_receipt) = if complete && safely_dormant {
@ -2926,6 +2952,8 @@ fn bind_persisted_lifecycle_receipt_at(
return Err("PERSONA_LIFECYCLE_RECEIPT_BINDING_CHANGED_REQUIRES_REINSPECTION".into());
}
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)?;
let expected_state = if persisted.outcome == "FAILED" {
"DORMANT_AFTER_FAILURE"
} else {
@ -2933,7 +2961,7 @@ fn bind_persisted_lifecycle_receipt_at(
};
let (_, observed_head) = exact_repository(&repository)?;
if record.state != expected_state
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
|| terminal_event.kind != "DORMANT"
|| primary_lease_held_by_session(runtime_root, &record)?
|| observed_head != record.git_head
|| require_clean_repository(&repository).is_err()
@ -4288,6 +4316,198 @@ mod tests {
);
}
#[test]
fn rejects_a_rehashed_failure_receipt_with_the_wrong_terminal_event() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let failed = 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, |_, _| {
Err("bounded provider failure".into())
})
},
)
.unwrap();
let session_id = failed.failure.as_ref().unwrap().session_id.clone();
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.failure.as_mut().unwrap().terminal_event_hash = "0".repeat(64);
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_FAILURE_TERMINAL_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 rejects_a_rehashed_failure_receipt_with_forged_attribution() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let failed = 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, |_, _| {
Err("bounded provider failure".into())
})
},
)
.unwrap();
let session_id = failed.failure.as_ref().unwrap().session_id.clone();
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
.failure
.as_mut()
.unwrap()
.attribution
.persona_cognitive_author = "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 = inspect_lifecycle_request_at(runtime.path(), &input).unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_FAILURE_TERMINAL_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_bind_rehashed_failure_evidence_changed_after_inspection() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let failed = 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, |_, _| {
Err("bounded provider failure".into())
})
},
)
.unwrap();
let session_id = failed.failure.as_ref().unwrap().session_id.clone();
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.failure.as_mut().unwrap().terminal_event_hash = "0".repeat(64);
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_FAILURE_TERMINAL_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_failure_evidence_bound_into_the_session() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let failed = 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, |_, _| {
Err("bounded provider failure".into())
})
},
)
.unwrap();
let session_id = failed.failure.as_ref().unwrap().session_id.clone();
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.failure.as_mut().unwrap().repository_path = "/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 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 terminal evidence must not rerun the organ"),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_FAILURE_TERMINAL_EVIDENCE_MISMATCH"));
}
#[test]
fn independently_promotes_only_the_current_verified_structured_checkpoint() {
let repo = persona_repo();