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
|
|
@ -173,6 +173,7 @@ non_ui_safe_organ_lifecycle_coordinator_source_implemented: 100
|
|||
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
|
||||
general_purpose_persona_runtime_implemented: 0
|
||||
human_live_projection_implemented: 0
|
||||
hololake_integrated: 0
|
||||
|
|
@ -195,6 +196,10 @@ runtime_health: 0
|
|||
终态、Git、租约、事件链和回执哈希全部复核后只重放原失败,不再次启动模型或器官。未完成
|
||||
闭合、脏仓库或仍持有租约的失败不能被伪装成终态回执。
|
||||
|
||||
成功回执与失败回执的每次重放都会重新读取规范仓库当前提交并检查工作树,而不是只信任回执
|
||||
生成时的状态。当前提交偏离会话记录或工作树变脏时,检查结果降级为人工复核,重放失败关闭,
|
||||
且不会重新启动器官。
|
||||
|
||||
人格器官现已拥有机器可读类型合同。系统能够在不唤醒人格、不取得主锁、不运行模型的情况下,
|
||||
检查 `FACT_SENSE`、`MEMORY_METABOLISM` 与 `EXECUTION_LIMB` 的固定模式、输入输出 schema、派生权限、
|
||||
模型推理边界、现实动作边界和真实可激活状态。只读事实感官与独立记忆代谢器官可激活;后者不
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ raw provider diagnostic. Replay and safe binding recovery require `DORMANT_AFTER
|
|||
event, a released lease, and an unchanged clean Git head. A failure that cannot prove those conditions is not
|
||||
terminal and remains recovery-required.
|
||||
|
||||
Repository state is revalidated at replay time for both successful and failed receipts. The current canonical
|
||||
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.
|
||||
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ clean Git head, semantic fingerprint, receipt hash, and session bindings before
|
|||
with `replayed: true`. Active, dirty, partially closed, or otherwise unproven failures retain the original
|
||||
recovery path and cannot be converted into a terminal receipt.
|
||||
|
||||
The unchanged-clean-repository condition is enforced again on every successful or failed receipt replay,
|
||||
not only when the receipt is first persisted. If the current head differs from the session's recorded head,
|
||||
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.
|
||||
|
||||
`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
|
||||
|
|
|
|||
|
|
@ -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