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:
parent
1cd8a1b489
commit
30d179e97a
7 changed files with 107 additions and 6 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue