← Files Canonical Memory VerifierARCHIVED FILE

integrations/codex-external-agent-memory-import/codex-import-preflight.patch

10.8 KB · Sep 30, 2026 · 23:13 UTC

↓ Download file

diff --git a/codex-rs/external-agent-migration/src/memory_import.rs b/codex-rs/external-agent-migration/src/memory_import.rs
index 92c38aa..f58ac29 100644
--- a/codex-rs/external-agent-migration/src/memory_import.rs
+++ b/codex-rs/external-agent-migration/src/memory_import.rs
@@ -50,6 +50,12 @@ struct ProjectScope<'a> {
     cwd: &'a Path,
 }
 
+#[derive(Debug, PartialEq, Eq)]
+struct PreparedMemoryFile {
+    relative_path: PathBuf,
+    content: Vec<u8>,
+}
+
 pub(super) async fn import(
     codex_home: &Path,
     external_agent_home: &Path,
@@ -284,21 +290,45 @@ fn replace_project_resources(
     project_cwd: &Path,
     memory_files: &[&ExternalMemoryFile],
 ) -> io::Result<()> {
+    replace_project_resources_with_preflight(
+        codex_home,
+        project_key,
+        project_cwd,
+        memory_files,
+        |_, _, _| Ok(None),
+    )
+    .map(|_| ())
+}
+
+fn replace_project_resources_with_preflight(
+    codex_home: &Path,
+    project_key: &str,
+    project_cwd: &Path,
+    memory_files: &[&ExternalMemoryFile],
+    preflight: impl FnOnce(&str, &Path, &[PreparedMemoryFile]) -> io::Result<Option<String>>,
+) -> io::Result<Option<String>> {
     let source_files = memory_files
         .iter()
         .map(|memory_file| {
-            fs::read(&memory_file.source_path)
-                .map(|content| (memory_file.relative_path.clone(), content))
+            fs::read(&memory_file.source_path).map(|content| PreparedMemoryFile {
+                relative_path: memory_file.relative_path.clone(),
+                content,
+            })
         })
         .collect::<io::Result<Vec<_>>>()?;
     let scope_content = project_scope_content(project_cwd)?;
 
+    // A verifier is deliberately called after every source byte is prepared but
+    // before the previous imported project is removed. This keeps ordinary
+    // Markdown imports unchanged while providing a fail-closed integration seam.
+    let conformance_digest = preflight(project_key, project_cwd, &source_files)?;
+
     remove_project_resources(codex_home, project_key)?;
     let target_root = resources_root(codex_home).join(project_key);
     fs::create_dir_all(&target_root)?;
     fs::write(target_root.join(PROJECT_SCOPE_FILE), scope_content)?;
-    for (relative_path, content) in source_files {
-        let target_path = target_root.join(relative_path);
+    for source_file in source_files {
+        let target_path = target_root.join(source_file.relative_path);
         let target_parent = target_path.parent().ok_or_else(|| {
             invalid_data_error(format!(
                 "memory target path has no parent: {}",
@@ -306,9 +336,9 @@ fn replace_project_resources(
             ))
         })?;
         fs::create_dir_all(target_parent)?;
-        fs::write(target_path, content)?;
+        fs::write(target_path, source_file.content)?;
     }
-    Ok(())
+    Ok(conformance_digest)
 }
 
 fn remove_project_resources(codex_home: &Path, project_key: &str) -> io::Result<bool> {
diff --git a/codex-rs/external-agent-migration/src/memory_import_tests.rs b/codex-rs/external-agent-migration/src/memory_import_tests.rs
index 9bf57f3..7d9729c 100644
--- a/codex-rs/external-agent-migration/src/memory_import_tests.rs
+++ b/codex-rs/external-agent-migration/src/memory_import_tests.rs
@@ -1,7 +1,37 @@
 use super::*;
 use pretty_assertions::assert_eq;
+use std::process::Command;
 use tempfile::TempDir;
 
+fn verify_canonical_fixture(fixture: &str) -> io::Result<String> {
+    let repository_root = std::env::var_os("CMV_REPOSITORY_ROOT")
+        .map(PathBuf::from)
+        .ok_or_else(|| invalid_data_error("CMV_REPOSITORY_ROOT is required"))?;
+    let python = std::env::var_os("CMV_PYTHON").unwrap_or_else(|| "python3".into());
+    let output = Command::new(python)
+        .arg("-B")
+        .arg(repository_root.join("skills/verify-canonical-memory/scripts/cmverify.py"))
+        .arg("verify")
+        .arg(repository_root.join("fixtures").join(fixture))
+        .output()?;
+    let result: serde_json::Value = serde_json::from_slice(&output.stdout)
+        .map_err(|_| invalid_data_error("cmverify did not emit strict JSON"))?;
+    if output.status.success()
+        && result.get("status").and_then(|value| value.as_str()) == Some("VERIFIED")
+    {
+        return result
+            .pointer("/digests/conformance_sha256")
+            .and_then(|value| value.as_str())
+            .map(str::to_string)
+            .ok_or_else(|| invalid_data_error("verified result omitted conformance digest"));
+    }
+    let code = result
+        .pointer("/errors/0/code")
+        .and_then(|value| value.as_str())
+        .unwrap_or("CMV_INTERNAL_FAILURE");
+    Err(invalid_data_error(code))
+}
+
 fn write_project_session(project_root: &Path, project_cwd: &Path) {
     fs::create_dir_all(project_cwd).expect("create project cwd");
     fs::write(
@@ -333,3 +363,154 @@ fn removes_an_existing_unscoped_target_when_cwd_is_unavailable() {
     );
     assert!(!target_root.exists());
 }
+
+#[test]
+fn preflight_failure_preserves_previous_imported_project() {
+    let root = TempDir::new().expect("create tempdir");
+    let codex_home = root.path().join(".codex");
+    let project_cwd = root.path().join("project-a-cwd");
+    fs::create_dir_all(&project_cwd).expect("create project cwd");
+
+    let target_root = resources_root(&codex_home).join("project-a");
+    fs::create_dir_all(&target_root).expect("create previous imported project");
+    fs::write(target_root.join("MEMORY.md"), b"previous verified memory")
+        .expect("write previous memory");
+    fs::write(
+        target_root.join(PROJECT_SCOPE_FILE),
+        project_scope_content(&project_cwd).expect("serialize project scope"),
+    )
+    .expect("write previous scope");
+
+    let source_path = root.path().join("candidate-MEMORY.md");
+    fs::write(&source_path, b"candidate memory").expect("write candidate memory");
+    let memory_file = ExternalMemoryFile {
+        project_key: "project-a".to_string(),
+        project_cwd: Some(project_cwd.clone()),
+        source_path,
+        relative_path: PathBuf::from("MEMORY.md"),
+    };
+
+    let error = replace_project_resources_with_preflight(
+        &codex_home,
+        "project-a",
+        &project_cwd,
+        &[&memory_file],
+        |project_key, checked_cwd, prepared_files| {
+            assert_eq!(project_key, "project-a");
+            assert_eq!(checked_cwd, project_cwd);
+            assert_eq!(
+                prepared_files,
+                &[PreparedMemoryFile {
+                    relative_path: PathBuf::from("MEMORY.md"),
+                    content: b"candidate memory".to_vec(),
+                }]
+            );
+            Err(invalid_data_error("CMV_SOURCE_HASH_MISMATCH"))
+        },
+    )
+    .expect_err("reject invalid candidate before replacement");
+
+    assert_eq!(error.kind(), io::ErrorKind::InvalidData);
+    assert_eq!(error.to_string(), "CMV_SOURCE_HASH_MISMATCH");
+    assert_eq!(
+        fs::read(target_root.join("MEMORY.md")).expect("read preserved memory"),
+        b"previous verified memory"
+    );
+    assert_eq!(
+        fs::read(target_root.join(PROJECT_SCOPE_FILE)).expect("read preserved scope"),
+        project_scope_content(&project_cwd).expect("serialize expected scope")
+    );
+}
+
+#[test]
+fn canonical_verifier_accepts_valid_bundle_with_stable_digest() {
+    let root = TempDir::new().expect("create tempdir");
+    let codex_home = root.path().join(".codex");
+    let project_cwd = root.path().join("project-a-cwd");
+    fs::create_dir_all(&project_cwd).expect("create project cwd");
+    let source_path = root.path().join("candidate-MEMORY.md");
+    fs::write(&source_path, b"candidate memory").expect("write candidate memory");
+    let memory_file = ExternalMemoryFile {
+        project_key: "project-a".to_string(),
+        project_cwd: Some(project_cwd.clone()),
+        source_path,
+        relative_path: PathBuf::from("MEMORY.md"),
+    };
+    let expected_digest = verify_canonical_fixture("valid/basic").expect("verify valid bundle");
+
+    let actual_digest = replace_project_resources_with_preflight(
+        &codex_home,
+        "project-a",
+        &project_cwd,
+        &[&memory_file],
+        |_, _, _| verify_canonical_fixture("valid/basic").map(Some),
+    )
+    .expect("import verified candidate");
+
+    assert_eq!(actual_digest, Some(expected_digest));
+    assert_eq!(
+        fs::read(resources_root(&codex_home).join("project-a/MEMORY.md"))
+            .expect("read imported candidate"),
+        b"candidate memory"
+    );
+}
+
+#[test]
+fn canonical_verifier_rejections_preserve_previous_import() {
+    let root = TempDir::new().expect("create tempdir");
+    let codex_home = root.path().join(".codex");
+    let project_cwd = root.path().join("project-a-cwd");
+    fs::create_dir_all(&project_cwd).expect("create project cwd");
+    let target_root = resources_root(&codex_home).join("project-a");
+    fs::create_dir_all(&target_root).expect("create previous imported project");
+    let source_path = root.path().join("candidate-MEMORY.md");
+    fs::write(&source_path, b"candidate memory").expect("write candidate memory");
+    let memory_file = ExternalMemoryFile {
+        project_key: "project-a".to_string(),
+        project_cwd: Some(project_cwd.clone()),
+        source_path,
+        relative_path: PathBuf::from("MEMORY.md"),
+    };
+    let cases = [
+        ("invalid/source-tampered", "CMV_SOURCE_HASH_MISMATCH"),
+        ("invalid/ambiguous-heads", "CMV_CURRENT_HEAD_AMBIGUOUS"),
+        (
+            "invalid/approval-true-expired",
+            "CMV_APPROVAL_ELIGIBILITY_VIOLATION",
+        ),
+        (
+            "invalid/approval-true-unexpired",
+            "CMV_APPROVAL_ELIGIBILITY_VIOLATION",
+        ),
+    ];
+
+    for (fixture, expected_code) in cases {
+        fs::write(target_root.join("MEMORY.md"), b"previous verified memory")
+            .expect("restore previous memory");
+        fs::write(
+            target_root.join(PROJECT_SCOPE_FILE),
+            project_scope_content(&project_cwd).expect("serialize project scope"),
+        )
+        .expect("restore previous scope");
+
+        let error = replace_project_resources_with_preflight(
+            &codex_home,
+            "project-a",
+            &project_cwd,
+            &[&memory_file],
+            |_, _, _| verify_canonical_fixture(fixture).map(Some),
+        )
+        .expect_err("reject invalid candidate before replacement");
+
+        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
+        assert_eq!(error.to_string(), expected_code);
+        assert_eq!(
+            fs::read(target_root.join("MEMORY.md")).expect("read preserved memory"),
+            b"previous verified memory"
+        );
+        assert_eq!(
+            fs::read(target_root.join(PROJECT_SCOPE_FILE)).expect("read preserved scope"),
+            project_scope_content(&project_cwd).expect("serialize expected scope")
+        );
+    }
+}

SHA-256: d2ad4cce7c7355b8e6d3425e6bfb465cbdfdf0ed8386589a65282bb4d5fb6adb