fix(pncc): revalidate receipt binding 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:19:28 +08:00
commit 21d120d7b5
8 changed files with 199 additions and 13 deletions

View file

@ -175,6 +175,7 @@ incomplete_idempotent_request_inspection_and_safe_receipt_recovery_source_implem
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
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0

View file

@ -37,6 +37,12 @@ interruption at the recorded head or a single persona-attributed checkpoint comm
checkpoint identity and commit trailers all match the session. Dirty repositories, unknown Git advances,
broken journals and attribution mismatches retain the lease and require manual review.
Idempotent lifecycle receipt recovery treats inspection as evidence, not as a lasting write permit. Immediately
before binding an interrupted persisted receipt, the kernel re-reads and verifies its schema, request identity,
semantic fingerprint, payload hash and persona identity, then re-checks the terminal event chain, dormant state,
released lease, canonical repository, recorded Git head and clean work tree. A receipt or repository changed
after inspection fails closed before any request or receipt-hash binding is written into the session record.
The manifest now has a typed organ-contract extension. The native inspection command returns normalized
contracts for `FACT_SENSE`, `MEMORY_METABOLISM`, and `EXECUTION_LIMB`, including fixed mode, input/output
schemas, derived permissions, inference/reality-action boundaries, and the actual activation state. The fact

View file

@ -2873,30 +2873,83 @@ fn recover_lifecycle_request_at(
inspection.status
));
}
let receipt_path =
session_directory(runtime_root, &inspection.session_id)?.join("lifecycle-receipt.json");
bind_persisted_lifecycle_receipt_at(runtime_root, input, &inspection)
}
fn bind_persisted_lifecycle_receipt_at(
runtime_root: &Path,
input: &PersonaLifecycleRunInput,
inspection: &PersonaLifecycleRequestInspectionReceipt,
) -> Result<PersonaLifecycleCommandReceipt, String> {
let (request_id, request_fingerprint, session_id, repository) =
lifecycle_request_identity(input)?;
if !inspection.safe_to_bind_receipt
|| inspection.request_id != request_id
|| inspection.request_fingerprint != request_fingerprint
|| inspection.session_id != session_id
{
return Err("PERSONA_LIFECYCLE_RECEIPT_BINDING_CHANGED_REQUIRES_REINSPECTION".into());
}
let receipt_path = session_directory(runtime_root, &session_id)?.join("lifecycle-receipt.json");
let persisted: PersistedPersonaLifecycleReceipt = serde_json::from_slice(
&fs::read(&receipt_path)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_READ_FAILED: {error}"))?,
)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_INVALID: {error}"))?;
let mut record = load_session_record(runtime_root, &inspection.session_id)?;
if persisted.schema != "hololake.pncc-persisted-lifecycle-receipt/v1"
|| persisted.request_id != request_id
{
return Err("PERSONA_LIFECYCLE_REQUEST_ID_MISMATCH".into());
}
if persisted.request_fingerprint != request_fingerprint {
return Err("PERSONA_LIFECYCLE_REQUEST_CONFLICT".into());
}
if hex_digest(&persisted_lifecycle_payload_bytes(&persisted)?)
!= persisted.lifecycle_receipt_hash
{
return Err("PERSONA_LIFECYCLE_RECEIPT_HASH_MISMATCH".into());
}
let mut record = load_session_record(runtime_root, &session_id)?;
let recorded_repository = Path::new(&record.repository_path)
.canonicalize()
.map_err(|error| format!("PERSONA_REPOSITORY_UNAVAILABLE: {error}"))?;
if recorded_repository != repository || record.persona_id != input.wake.expected_persona_id {
return Err("PERSONA_LIFECYCLE_REQUEST_SESSION_IDENTITY_MISMATCH".into());
}
if !persisted_lifecycle_identity_matches(&persisted, &session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
if record.request_id.is_some()
|| record.request_fingerprint.is_some()
|| record.lifecycle_receipt_hash.is_some()
{
return Err("PERSONA_LIFECYCLE_RECEIPT_BINDING_CHANGED_REQUIRES_REINSPECTION".into());
}
record.request_id = Some(inspection.request_id.clone());
record.request_fingerprint = Some(inspection.request_fingerprint.clone());
let events = verify_event_journal(runtime_root, &record)?;
let expected_state = if persisted.outcome == "FAILED" {
"DORMANT_AFTER_FAILURE"
} else {
"DORMANT"
};
let (_, observed_head) = exact_repository(&repository)?;
if record.state != expected_state
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
|| primary_lease_held_by_session(runtime_root, &record)?
|| observed_head != record.git_head
|| require_clean_repository(&repository).is_err()
{
return Err("PERSONA_LIFECYCLE_RECEIPT_BINDING_CHANGED_REQUIRES_REINSPECTION".into());
}
record.request_id = Some(request_id.clone());
record.request_fingerprint = Some(request_fingerprint.clone());
record.lifecycle_receipt_hash = Some(persisted.lifecycle_receipt_hash);
write_session_record(runtime_root, &record)?;
verified_lifecycle_replay(
runtime_root,
&inspection.request_id,
&inspection.request_fingerprint,
&inspection.session_id,
Path::new(&record.repository_path),
&request_id,
&request_fingerprint,
&session_id,
&repository,
)?
.ok_or_else(|| "PERSONA_LIFECYCLE_RECEIPT_RECOVERY_LOST_SESSION".into())
}
@ -4000,6 +4053,92 @@ mod tests {
assert!(!after.safe_to_bind_receipt);
}
#[test]
fn refuses_to_bind_a_receipt_tampered_after_safe_inspection() {
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":"Recoverable receipt.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let (_, _, session_id, _) = lifecycle_request_identity(&input).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: serde_json::Value =
serde_json::from_slice(&fs::read(&receipt_path).unwrap()).unwrap();
persisted["lifecycle"]["personaId"] = serde_json::Value::String("TAMPERED".into());
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_RECEIPT_HASH_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_after_the_repository_advances_from_safe_inspection() {
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":"Recoverable receipt.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hldp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
let (_, _, session_id, _) = lifecycle_request_identity(&input).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);
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 error =
bind_persisted_lifecycle_receipt_at(runtime.path(), &input, &inspection).unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_RECEIPT_BINDING_CHANGED_REQUIRES_REINSPECTION"));
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_fabricate_a_receipt_for_a_session_interrupted_before_receipt_persistence() {
let repo = persona_repo();