fix(pncc): revalidate Git state before replay
Human-Responsibility: ICE-GL∞ / 冰朔 Persona-Author: ICE-P-ZY001 / 铸渊 Execution-Runtime: Codex macOS Development-ID: DEV-20260810-014 Authorization-Scope: GH-PNCC local runtime and REPO-014 publication Source-Anchor: UI and execution limb deferred
This commit is contained in:
parent
abc7dbd625
commit
b067c7bcaa
10 changed files with 143 additions and 8 deletions
|
|
@ -2720,6 +2720,11 @@ fn verified_lifecycle_replay(
|
|||
{
|
||||
return Err("PERSONA_LIFECYCLE_RECEIPT_SESSION_BINDING_MISMATCH".into());
|
||||
}
|
||||
let (_, observed_head) = exact_repository(repository)?;
|
||||
if observed_head != record.git_head {
|
||||
return Err("PERSONA_LIFECYCLE_REPLAY_GIT_HEAD_MISMATCH".into());
|
||||
}
|
||||
require_clean_repository(repository)?;
|
||||
let events = verify_event_journal(runtime_root, &record)?;
|
||||
let expected_state = if persisted.outcome == "FAILED" {
|
||||
"DORMANT_AFTER_FAILURE"
|
||||
|
|
@ -2829,9 +2834,13 @@ fn inspect_lifecycle_request_at(
|
|||
} else {
|
||||
"DORMANT"
|
||||
};
|
||||
let (_, observed_head) = exact_repository(&repository)?;
|
||||
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")
|
||||
&& !lease_held;
|
||||
&& !lease_held
|
||||
&& repository_matches_record;
|
||||
let (status, safe_to_bind_receipt) = if complete && safely_dormant {
|
||||
("COMPLETE_REPLAYABLE", false)
|
||||
} else if unbound && safely_dormant {
|
||||
|
|
@ -3708,6 +3717,39 @@ mod tests {
|
|||
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_to_replay_a_completed_lifecycle_when_the_repository_is_dirty() {
|
||||
let repo = persona_repo();
|
||||
let runtime = tempfile::TempDir::new().unwrap();
|
||||
let input = lifecycle_fact_input(repo.path());
|
||||
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":"Fact before repository drift.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(repo.path().join("untracked-after-lifecycle.txt"), "dirty\n").unwrap();
|
||||
|
||||
let inspection = inspect_lifecycle_request_at(runtime.path(), &input).unwrap();
|
||||
assert_eq!(inspection.status, "MANUAL_REVIEW_REQUIRED");
|
||||
assert!(!inspection.safe_to_bind_receipt);
|
||||
let error = run_idempotent_lifecycle_at(
|
||||
runtime.path(),
|
||||
input,
|
||||
"2026-08-11T00:00:02.000Z",
|
||||
"2026-08-11T00:00:03.000Z",
|
||||
|_, _, _| panic!("a dirty repository must not rerun the organ"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("PERSONA_REPOSITORY_DIRTY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_reusing_a_lifecycle_request_id_for_a_different_operation() {
|
||||
let repo = persona_repo();
|
||||
|
|
@ -3908,6 +3950,41 @@ mod tests {
|
|||
assert!(!runtime.path().join("leases/ICE-P-ZY001.json").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_to_replay_a_terminal_failure_after_the_repository_head_advances() {
|
||||
let repo = persona_repo();
|
||||
let runtime = tempfile::TempDir::new().unwrap();
|
||||
let input = lifecycle_fact_input(repo.path());
|
||||
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();
|
||||
fs::write(repo.path().join("advanced.txt"), "new head\n").unwrap();
|
||||
run_git(repo.path(), &["add", "advanced.txt"]);
|
||||
run_git(repo.path(), &["commit", "-m", "advance persona repository"]);
|
||||
|
||||
let inspection = inspect_lifecycle_request_at(runtime.path(), &input).unwrap();
|
||||
assert_eq!(inspection.status, "MANUAL_REVIEW_REQUIRED");
|
||||
assert!(!inspection.safe_to_bind_receipt);
|
||||
let error = run_idempotent_lifecycle_at(
|
||||
runtime.path(),
|
||||
input,
|
||||
"2026-08-11T00:00:02.000Z",
|
||||
"2026-08-11T00:00:03.000Z",
|
||||
|_, _, _| panic!("an advanced repository must not rerun the organ"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("PERSONA_LIFECYCLE_REPLAY_GIT_HEAD_MISMATCH"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safely_recovers_an_unbound_terminal_failure_receipt() {
|
||||
let repo = persona_repo();
|
||||
|
|
|
|||
Loading…
Reference in a new issue