fix: bind failure receipt error code

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:41:52 +08:00
commit 30d179e97a
7 changed files with 107 additions and 6 deletions

View file

@ -445,6 +445,8 @@ struct PersonaSessionRecord {
attribution: PersonaAttribution,
state: String,
#[serde(default)]
terminal_failure_code: Option<String>,
#[serde(default)]
request_id: Option<String>,
#[serde(default)]
request_fingerprint: Option<String>,
@ -1347,6 +1349,7 @@ fn prepare_wake_at(
fact_source_paths: organ.paths.clone(),
attribution: input.attribution.clone(),
state: "BOUND_NOT_INFERENCING".into(),
terminal_failure_code: None,
request_id: None,
request_fingerprint: None,
lifecycle_receipt_hash: None,
@ -2722,6 +2725,7 @@ fn validate_persisted_lifecycle_terminal_evidence(
if failure.repository_path != record.repository_path
|| failure.terminal_event_hash != terminal_event.event_hash
|| failure.attribution != record.attribution
|| record.terminal_failure_code.as_deref() != Some(failure.error_code.as_str())
|| lifecycle_failure_code(&failure.error_code) != failure.error_code
{
return Err("PERSONA_LIFECYCLE_FAILURE_TERMINAL_EVIDENCE_MISMATCH".into());
@ -3308,12 +3312,23 @@ fn persist_terminal_lifecycle_failure(
{
return Err("PERSONA_LIFECYCLE_FAILURE_BINDING_ALREADY_PRESENT".into());
}
let error_code = lifecycle_failure_code(error);
match record.terminal_failure_code.as_deref() {
Some(recorded) if recorded != error_code => {
return Err("PERSONA_LIFECYCLE_FAILURE_CODE_ALREADY_BOUND".into());
}
Some(_) => {}
None => {
record.terminal_failure_code = Some(error_code.clone());
write_session_record(runtime_root, &record)?;
}
}
let failure = PersonaLifecycleFailureReceipt {
schema: "hololake.pncc-lifecycle-failure-receipt/v1".into(),
session_id: session_id.into(),
persona_id: record.persona_id.clone(),
repository_path: record.repository_path.clone(),
error_code: lifecycle_failure_code(error),
error_code,
terminal_event_hash: terminal_event.event_hash.clone(),
attribution: record.attribution.clone(),
};
@ -5350,6 +5365,48 @@ mod tests {
assert!(record.lifecycle_receipt_hash.is_none());
}
#[test]
fn rejects_a_rehashed_failure_receipt_with_a_forged_error_code() {
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().error_code = "AUTHORIZATION_GRANTED".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"));
}
#[test]
fn refuses_to_bind_rehashed_failure_evidence_changed_after_inspection() {
let repo = persona_repo();