fix(pncc): revalidate terminal success evidence

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:
铸渊 / ICE-P-ZY001 2026-08-11 04:51:20 +08:00
commit e9ee0a8bb1
9 changed files with 219 additions and 38 deletions

View file

@ -174,6 +174,7 @@ idempotent_lifecycle_request_and_receipt_replay_source_implemented: 100
incomplete_idempotent_request_inspection_and_safe_receipt_recovery_source_implemented: 100
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
general_purpose_persona_runtime_implemented: 0
human_live_projection_implemented: 0
hololake_integrated: 0
@ -200,6 +201,10 @@ runtime_health: 0
生成时的状态。当前提交偏离会话记录或工作树变脏时,检查结果降级为人工复核,重放失败关闭,
且不会重新启动器官。
首次成功回执落盘前也必须重新验证事件链、最终休眠事件、租约释放、规范仓库、会话记录的
Git 提交、干净工作树与回执身份。器官已经完成不等于仍可签发当前成功回执;完成后仓库发生
漂移时不落盘、不绑定并失败关闭。
人格器官现已拥有机器可读类型合同。系统能够在不唤醒人格、不取得主锁、不运行模型的情况下,
检查 `FACT_SENSE``MEMORY_METABOLISM``EXECUTION_LIMB` 的固定模式、输入输出 schema、派生权限、
模型推理边界、现实动作边界和真实可激活状态。只读事实感官与独立记忆代谢器官可激活;后者不

View file

@ -114,6 +114,11 @@ Repository state is revalidated at replay time for both successful and failed re
repository must still be clean and its head must equal the session record; otherwise inspection returns
`MANUAL_REVIEW_REQUIRED` and ordinary replay fails without invoking the organ.
The first successful receipt is also conditional on freshly verified terminal evidence. Before persisting
and binding `COMPLETED`, the coordinator checks the event chain, final dormancy, released lease, canonical
repository, recorded head, clean worktree, and receipt identity. A completed organ is not enough to issue a
current lifecycle receipt after repository drift.
`PersonaRuntimeQueryReceipt` is a bounded projection of the durable runtime files, not another truth store.
It filters by the caller's expected persona and canonical repository, validates each matching event chain,
and returns at most 100 newest session summaries. Dormant sessions expose no active organ. The receipt keeps

View file

@ -86,6 +86,11 @@ not only when the receipt is first persisted. If the current head differs from t
or the worktree has become dirty, replay fails closed and request inspection reports manual review instead
of restarting the organ or treating stale evidence as current.
Successful receipt persistence now applies the same terminal-evidence boundary before the first receipt is
written. The kernel re-verifies the event journal and final `DORMANT` event, released lease, canonical
repository, recorded Git head, clean worktree, and lifecycle identity. Repository drift after organ
completion therefore cannot be bound or returned as a current `COMPLETED` command result.
`query_persona_code_channel_runtime` is the bounded read model for later projection surfaces. The caller must
name one exact persona and canonical repository and may request at most 100 sessions. The kernel reads the
existing session records and event journals directly, verifies every returned hash chain, sorts by the last

View file

@ -2978,6 +2978,70 @@ fn persist_terminal_lifecycle_failure(
}))
}
fn persist_successful_lifecycle_receipt(
runtime_root: &Path,
request_id: &str,
request_fingerprint: &str,
session_id: &str,
repository: &Path,
lifecycle: serde_json::Value,
) -> Result<PersonaLifecycleCommandReceipt, String> {
let lifecycle_bytes = serde_json::to_vec(&lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}"))?;
let lifecycle_receipt_hash = hex_digest(&lifecycle_bytes);
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 {
return Err("PERSONA_LIFECYCLE_REQUEST_SESSION_IDENTITY_MISMATCH".into());
}
let events = verify_event_journal(runtime_root, &record)?;
if record.state != "DORMANT"
|| events.last().map(|event| event.kind.as_str()) != Some("DORMANT")
|| primary_lease_held_by_session(runtime_root, &record)?
{
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
let (_, observed_head) = exact_repository(repository)?;
if observed_head != record.git_head {
return Err("PERSONA_LIFECYCLE_COMPLETION_GIT_HEAD_MISMATCH".into());
}
require_clean_repository(repository)?;
let persisted = PersistedPersonaLifecycleReceipt {
schema: "hololake.pncc-persisted-lifecycle-receipt/v1".into(),
request_id: request_id.into(),
request_fingerprint: request_fingerprint.into(),
lifecycle_receipt_hash: lifecycle_receipt_hash.clone(),
outcome: "COMPLETED".into(),
lifecycle: lifecycle.clone(),
failure: None,
};
if !persisted_lifecycle_identity_matches(&persisted, session_id, &record.persona_id) {
return Err("PERSONA_LIFECYCLE_RECEIPT_IDENTITY_MISMATCH".into());
}
// Persist the immutable full receipt before binding it into the mutable session record. A
// crash between these atomic writes leaves a verifiable, explicitly recoverable state.
write_json_file(
&session_directory(runtime_root, session_id)?.join("lifecycle-receipt.json"),
&persisted,
"PERSONA_LIFECYCLE_RECEIPT",
)?;
record.request_id = Some(request_id.into());
record.request_fingerprint = Some(request_fingerprint.into());
record.lifecycle_receipt_hash = Some(lifecycle_receipt_hash);
write_session_record(runtime_root, &record)?;
Ok(PersonaLifecycleCommandReceipt {
schema: "hololake.pncc-lifecycle-command-receipt/v1",
request_id: request_id.into(),
request_fingerprint: request_fingerprint.into(),
replayed: false,
outcome: "COMPLETED",
lifecycle,
failure: None,
})
}
fn run_idempotent_lifecycle_at<F>(
runtime_root: &Path,
input: PersonaLifecycleRunInput,
@ -3024,41 +3088,14 @@ where
};
let lifecycle = serde_json::to_value(lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_SERIALIZATION_FAILED: {error}"))?;
let lifecycle_bytes = serde_json::to_vec(&lifecycle)
.map_err(|error| format!("PERSONA_LIFECYCLE_RECEIPT_HASH_INPUT_FAILED: {error}"))?;
let lifecycle_receipt_hash = hex_digest(&lifecycle_bytes);
let mut record = load_session_record(runtime_root, &session_id)?;
if record.state != "DORMANT" || primary_lease_held_by_session(runtime_root, &record)? {
return Err("PERSONA_LIFECYCLE_REQUEST_INCOMPLETE_REQUIRES_RECOVERY".into());
}
// Persist the immutable full receipt before binding it into the mutable session record. A
// crash between these atomic writes leaves a verifiable, explicitly recoverable state.
write_json_file(
&session_directory(runtime_root, &session_id)?.join("lifecycle-receipt.json"),
&PersistedPersonaLifecycleReceipt {
schema: "hololake.pncc-persisted-lifecycle-receipt/v1".into(),
request_id: request_id.clone(),
request_fingerprint: request_fingerprint.clone(),
lifecycle_receipt_hash: lifecycle_receipt_hash.clone(),
outcome: "COMPLETED".into(),
lifecycle: lifecycle.clone(),
failure: None,
},
"PERSONA_LIFECYCLE_RECEIPT",
)?;
record.request_id = Some(request_id.clone());
record.request_fingerprint = Some(request_fingerprint.clone());
record.lifecycle_receipt_hash = Some(lifecycle_receipt_hash);
write_session_record(runtime_root, &record)?;
Ok(PersonaLifecycleCommandReceipt {
schema: "hololake.pncc-lifecycle-command-receipt/v1",
request_id,
request_fingerprint,
replayed: false,
outcome: "COMPLETED",
persist_successful_lifecycle_receipt(
runtime_root,
&request_id,
&request_fingerprint,
&session_id,
&repository,
lifecycle,
failure: None,
})
)
}
#[tauri::command]
@ -3750,6 +3787,97 @@ mod tests {
assert!(error.contains("PERSONA_REPOSITORY_DIRTY"));
}
#[test]
fn refuses_to_persist_success_when_the_repository_becomes_dirty_after_completion() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let (request_id, request_fingerprint, session_id, repository) =
lifecycle_request_identity(&input).unwrap();
let lifecycle = run_lifecycle_at(
runtime.path(),
input,
&session_id,
"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":"Completed before repository drift.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
fs::write(repo.path().join("dirty-before-receipt.txt"), "dirty\n").unwrap();
let error = persist_successful_lifecycle_receipt(
runtime.path(),
&request_id,
&request_fingerprint,
&session_id,
&repository,
serde_json::to_value(lifecycle).unwrap(),
)
.unwrap_err();
assert!(error.contains("PERSONA_REPOSITORY_DIRTY"));
assert!(!session_directory(runtime.path(), &session_id)
.unwrap()
.join("lifecycle-receipt.json")
.exists());
let record = load_session_record(runtime.path(), &session_id).unwrap();
assert!(record.request_id.is_none());
assert!(record.lifecycle_receipt_hash.is_none());
}
#[test]
fn refuses_to_persist_success_when_the_repository_head_advances_after_completion() {
let repo = persona_repo();
let runtime = tempfile::TempDir::new().unwrap();
let input = lifecycle_fact_input(repo.path());
let (request_id, request_fingerprint, session_id, repository) =
lifecycle_request_identity(&input).unwrap();
let lifecycle = run_lifecycle_at(
runtime.path(),
input,
&session_id,
"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":"Completed before a new commit.","facts":[{"statement":"The brain exists.","evidencePaths":["brain/CORE.hdlp"]}],"limitations":[]}"#.into())
})
},
)
.unwrap();
fs::write(
repo.path().join("advanced-before-receipt.txt"),
"new head\n",
)
.unwrap();
run_git(repo.path(), &["add", "advanced-before-receipt.txt"]);
run_git(
repo.path(),
&["commit", "-m", "advance before lifecycle receipt"],
);
let error = persist_successful_lifecycle_receipt(
runtime.path(),
&request_id,
&request_fingerprint,
&session_id,
&repository,
serde_json::to_value(lifecycle).unwrap(),
)
.unwrap_err();
assert!(error.contains("PERSONA_LIFECYCLE_COMPLETION_GIT_HEAD_MISMATCH"));
assert!(!session_directory(runtime.path(), &session_id)
.unwrap()
.join("lifecycle-receipt.json")
.exists());
let record = load_session_record(runtime.path(), &session_id).unwrap();
assert!(record.request_id.is_none());
assert!(record.lifecycle_receipt_hash.is_none());
}
#[test]
fn rejects_reusing_a_lifecycle_request_id_for_a_different_operation() {
let repo = persona_repo();