cortex-ai-memory 2.2.1__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/Cargo.lock +9 -5
  2. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/PKG-INFO +1 -1
  3. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/Cargo.toml +2 -1
  4. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/export.rs +7 -0
  5. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/lib.rs +34 -3
  6. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/crypto.rs +78 -0
  7. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/merge.rs +26 -0
  8. cortex_ai_memory-2.4.0/cortex-core/src/sync/mod.rs +1211 -0
  9. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/oplog.rs +13 -4
  10. cortex_ai_memory-2.4.0/cortex-core/src/sync/snapshot.rs +717 -0
  11. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/state.rs +125 -0
  12. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/types.rs +5 -0
  13. cortex_ai_memory-2.4.0/cortex-core/tests/test_reserved_namespace.rs +112 -0
  14. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_sync.rs +1 -1
  15. cortex_ai_memory-2.4.0/cortex-core/tests/test_sync_hardening.rs +554 -0
  16. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-python/Cargo.toml +2 -1
  17. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/pyproject.toml +1 -1
  18. cortex_ai_memory-2.2.1/cortex-core/src/sync/mod.rs +0 -687
  19. cortex_ai_memory-2.2.1/cortex-core/src/sync/snapshot.rs +0 -361
  20. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/Cargo.toml +0 -0
  21. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/README.md +0 -0
  22. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/benches/comparison_bench.rs +0 -0
  23. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/benches/memory_index_bench.rs +0 -0
  24. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/belief.rs +0 -0
  25. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/compression.rs +0 -0
  26. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/consolidation.rs +0 -0
  27. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/context.rs +0 -0
  28. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/embedder.rs +0 -0
  29. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/episode.rs +0 -0
  30. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/events.rs +0 -0
  31. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/inference.rs +0 -0
  32. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/people.rs +0 -0
  33. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin.rs +0 -0
  34. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin_manager.rs +0 -0
  35. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/mod.rs +0 -0
  36. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/tag_classifier.rs +0 -0
  37. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/procedural.rs +0 -0
  38. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/relationship.rs +0 -0
  39. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/retrieval.rs +0 -0
  40. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/semantic.rs +0 -0
  41. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/memory_index.rs +0 -0
  42. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/mod.rs +0 -0
  43. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/sqlite.rs +0 -0
  44. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/traits.rs +0 -0
  45. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/hlc.rs +0 -0
  46. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/provider.rs +0 -0
  47. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/secret.rs +0 -0
  48. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/watcher.rs +0 -0
  49. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/working.rs +0 -0
  50. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_belief.rs +0 -0
  51. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cache_and_perf.rs +0 -0
  52. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression.rs +0 -0
  53. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression_advanced.rs +0 -0
  54. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation.rs +0 -0
  55. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation_advanced.rs +0 -0
  56. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_context.rs +0 -0
  57. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_contradiction_flow.rs +0 -0
  58. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_e2e.rs +0 -0
  59. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_full.rs +0 -0
  60. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_episode.rs +0 -0
  61. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_events.rs +0 -0
  62. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_full_privacy_chain.rs +0 -0
  63. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_gdrive_real.rs +0 -0
  64. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_ingest_parity.rs +0 -0
  65. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_memory_index.rs +0 -0
  66. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_optimizations.rs +0 -0
  67. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_people.rs +0 -0
  68. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin.rs +0 -0
  69. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin_manager_ext.rs +0 -0
  70. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_privacy_sync.rs +0 -0
  71. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_procedural.rs +0 -0
  72. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval.rs +0 -0
  73. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval_advanced.rs +0 -0
  74. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_semantic.rs +0 -0
  75. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage.rs +0 -0
  76. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage_advanced.rs +0 -0
  77. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_temporal_retrieval.rs +0 -0
  78. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_traits_defaults.rs +0 -0
  79. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_types.rs +0 -0
  80. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_working_memory.rs +0 -0
  81. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-python/README.md +0 -0
  82. {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-python/src/lib.rs +0 -0
@@ -573,7 +573,7 @@ dependencies = [
573
573
 
574
574
  [[package]]
575
575
  name = "cortex-core"
576
- version = "2.2.1"
576
+ version = "2.4.0"
577
577
  dependencies = [
578
578
  "aes-gcm",
579
579
  "argon2",
@@ -606,7 +606,7 @@ dependencies = [
606
606
 
607
607
  [[package]]
608
608
  name = "cortex-http"
609
- version = "2.2.1"
609
+ version = "2.4.0"
610
610
  dependencies = [
611
611
  "axum",
612
612
  "chrono",
@@ -623,16 +623,19 @@ dependencies = [
623
623
 
624
624
  [[package]]
625
625
  name = "cortex-mcp-server"
626
- version = "2.2.1"
626
+ version = "2.4.0"
627
627
  dependencies = [
628
+ "axum",
628
629
  "chrono",
629
630
  "clap",
630
631
  "cortex-core",
631
632
  "hostname",
633
+ "libc",
632
634
  "rand 0.8.5",
633
635
  "serde",
634
636
  "serde_json",
635
637
  "tokio",
638
+ "tower",
636
639
  "tracing",
637
640
  "tracing-subscriber",
638
641
  "uuid",
@@ -640,7 +643,7 @@ dependencies = [
640
643
 
641
644
  [[package]]
642
645
  name = "cortex-python"
643
- version = "2.2.1"
646
+ version = "2.4.0"
644
647
  dependencies = [
645
648
  "cortex-core",
646
649
  "pyo3",
@@ -649,7 +652,7 @@ dependencies = [
649
652
 
650
653
  [[package]]
651
654
  name = "cortex-wasm"
652
- version = "2.2.1"
655
+ version = "2.4.0"
653
656
  dependencies = [
654
657
  "chrono",
655
658
  "js-sys",
@@ -3485,6 +3488,7 @@ dependencies = [
3485
3488
  "pin-project-lite",
3486
3489
  "sync_wrapper",
3487
3490
  "tokio",
3491
+ "tokio-util",
3488
3492
  "tower-layer",
3489
3493
  "tower-service",
3490
3494
  "tracing",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortex-ai-memory
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "cortex-core"
3
- version = "2.2.1"
3
+ version = "2.4.0"
4
4
  edition = "2021"
5
+ rust-version = "1.89"
5
6
 
6
7
  [features]
7
8
  default = ["encrypted-db"]
@@ -117,6 +117,13 @@ pub fn import_all(
117
117
  };
118
118
 
119
119
  if let Some(memories) = data.memories {
120
+ // The Muse export is consent given on this device. Import never recreates it:
121
+ // a crafted file could otherwise plant rows Muse would serve. After a restore, the
122
+ // user re-exports with `gateway allow`.
123
+ let memories: Vec<_> = memories
124
+ .into_iter()
125
+ .filter(|m| m.namespace.as_deref() != Some(crate::types::MUSE_EXPORT_NAMESPACE))
126
+ .collect();
120
127
  report.memories = memories.len();
121
128
  for mem in &memories {
122
129
  storage.store_memory(mem)?;
@@ -559,10 +559,17 @@ impl Cortex {
559
559
  Ok(())
560
560
  }
561
561
 
562
+ /// Embed `text` with the local model, or `None` when embeddings are compiled out,
563
+ /// disabled via `CORTEX_NO_EMBEDDINGS`, or the model failed to load.
564
+ pub fn embed_query(&self, text: &str) -> Option<Vec<f32>> {
565
+ self.auto_embed(text, None)
566
+ }
567
+
562
568
  /// Auto-generate embedding if embedder is available and none provided.
563
569
  /// Lazily initializes the embedding model on first call.
564
- #[allow(unused_variables)]
565
570
  fn auto_embed(&self, text: &str, embedding: Option<Vec<f32>>) -> Option<Vec<f32>> {
571
+ #[cfg(not(feature = "embeddings"))]
572
+ let _ = text;
566
573
  if embedding.is_some() {
567
574
  return embedding;
568
575
  }
@@ -721,6 +728,12 @@ impl Cortex {
721
728
  people: &PeopleGraph<'_>,
722
729
  pending_hashes: &[String],
723
730
  ) -> Result<PrepareOutcome, CortexError> {
731
+ if namespace == Some(crate::types::MUSE_EXPORT_NAMESPACE) {
732
+ return Err(CortexError::InvalidInput(format!(
733
+ "namespace '{}' is reserved for `cortex-mcp-server gateway allow`",
734
+ crate::types::MUSE_EXPORT_NAMESPACE
735
+ )));
736
+ }
724
737
  let mut source = MemSource::new(channel);
725
738
  let embedding = self.auto_embed(text, embedding);
726
739
 
@@ -804,8 +817,19 @@ impl Cortex {
804
817
  return None;
805
818
  }
806
819
  let emb = embedding.as_ref()?;
807
- if let Some((existing_id, sim)) = self.index.search(emb, 1).first().copied() {
808
- if sim >= self.dedup_config.near_dedup_threshold {
820
+ // A Muse export copy is not the user's memory: merging into it would drop the new
821
+ // ingest (and a later `gateway revoke` would lose the only copy). Skip export copies
822
+ // and keep looking at the next-nearest neighbours instead of giving up.
823
+ const NEAR_DEDUP_CANDIDATES: usize = 8;
824
+ let is_export_copy = |id: Uuid| {
825
+ matches!(self.storage.get_memory(id),
826
+ Ok(Some(m)) if m.namespace.as_deref() == Some(crate::types::MUSE_EXPORT_NAMESPACE))
827
+ };
828
+ for (existing_id, sim) in self.index.search(emb, NEAR_DEDUP_CANDIDATES) {
829
+ if sim < self.dedup_config.near_dedup_threshold {
830
+ break; // results are ordered by similarity
831
+ }
832
+ if !is_export_copy(existing_id) {
809
833
  return Some(NearDup::Existing(existing_id));
810
834
  }
811
835
  }
@@ -1442,6 +1466,13 @@ impl Cortex {
1442
1466
  .storage
1443
1467
  .get_memory(id)?
1444
1468
  .ok_or_else(|| CortexError::NotFound(format!("memory {id}")))?;
1469
+ // Muse export copies are local-only consent records; they must never reach sync.
1470
+ if privacy.is_syncable() && mem.namespace.as_deref() == Some(crate::types::MUSE_EXPORT_NAMESPACE) {
1471
+ return Err(CortexError::InvalidInput(format!(
1472
+ "memories in the reserved '{}' namespace cannot be made syncable",
1473
+ crate::types::MUSE_EXPORT_NAMESPACE
1474
+ )));
1475
+ }
1445
1476
  let was_syncable = mem.privacy.is_syncable();
1446
1477
  mem.privacy = privacy;
1447
1478
  self.storage.update_memory(&mem)?;
@@ -113,6 +113,25 @@ pub struct EncryptionManifest {
113
113
  pub hmac_salt: Option<String>, // base64-encoded salt for HMAC key derivation
114
114
  #[serde(skip_serializing_if = "Option::is_none")]
115
115
  pub hmac: Option<String>, // base64-encoded HMAC of manifest content (computed without hmac and hmac_salt fields)
116
+ /// The snapshot new devices must bootstrap from. Covered by the manifest HMAC, so an
117
+ /// attacker without the passphrase can neither replay an older snapshot (same name, older
118
+ /// content) nor make restore fall back to one. Omitted when `None`, which keeps the HMAC
119
+ /// input of manifests written before this field existed byte-identical.
120
+ #[serde(default, skip_serializing_if = "Option::is_none")]
121
+ pub latest_snapshot: Option<SnapshotPointer>,
122
+ /// Monotonic counter bumped on every manifest write (first creation = 1). Covered by the
123
+ /// HMAC; devices remember the highest value they accepted and reject lower ones, so a
124
+ /// replayed older manifest (with its older snapshot pointer or key version) is detected.
125
+ /// Absent in legacy manifests (treated as 0); omitted when `None` for HMAC compatibility.
126
+ #[serde(default, skip_serializing_if = "Option::is_none")]
127
+ pub generation: Option<u64>,
128
+ }
129
+
130
+ /// Names the authoritative encrypted snapshot: its file name and its `HMAC:` line value.
131
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132
+ pub struct SnapshotPointer {
133
+ pub file: String,
134
+ pub mac: String,
116
135
  }
117
136
 
118
137
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -140,6 +159,8 @@ pub fn new_encryption_manifest() -> EncryptionManifest {
140
159
  key_version: Some(0), // Start at version 0; higher versions for rotated keys
141
160
  hmac_salt: None, // Will be computed during sync initialization
142
161
  hmac: None, // HMAC will be computed during sync initialization
162
+ latest_snapshot: None,
163
+ generation: None,
143
164
  }
144
165
  }
145
166
 
@@ -285,6 +306,19 @@ pub fn decrypt_line(ctx: &CryptoContext, encrypted_line: &str) -> Result<Vec<u8>
285
306
  }
286
307
  }
287
308
 
309
+ /// Key version of an envelope: `ENC1` is version 0; `ENC2` carries it in its first two
310
+ /// payload bytes. `None` if the line is not a well-formed envelope header. Only the first
311
+ /// base64 quantum is decoded, so this is cheap even for a large snapshot envelope.
312
+ pub fn envelope_version(line: &str) -> Option<u32> {
313
+ if line.starts_with(ENC_PREFIX) {
314
+ return Some(0);
315
+ }
316
+ let head = line.strip_prefix(ENC2_PREFIX)?.get(..4)?;
317
+ let bytes = base64::engine::general_purpose::STANDARD.decode(head).ok()?;
318
+ let version = bytes.get(..VERSION_LEN)?;
319
+ Some(u16::from_le_bytes([version[0], version[1]]) as u32)
320
+ }
321
+
288
322
  /// Check if a line is encrypted (either envelope version).
289
323
  pub fn is_encrypted_line(line: &str) -> bool {
290
324
  line.starts_with(ENC_PREFIX) || line.starts_with(ENC2_PREFIX)
@@ -588,4 +622,48 @@ mod tests {
588
622
  assert!(is_encrypted_line("ENC1:abc123"));
589
623
  assert!(!is_encrypted_line("{\"op_id\":\"...\"}"));
590
624
  }
625
+ #[test]
626
+ fn test_envelope_version() {
627
+ let m = test_manifest();
628
+ let ctx0 = derive_key("pass", &m).unwrap();
629
+ let ctx3 = derive_key("pass", &manifest_at_version(&m, 3)).unwrap();
630
+ assert_eq!(envelope_version(&encrypt_line(&ctx0, b"x").unwrap()), Some(0));
631
+ assert_eq!(envelope_version(&encrypt_line(&ctx3, b"x").unwrap()), Some(3));
632
+ assert_eq!(envelope_version("ENC2:"), None);
633
+ assert_eq!(envelope_version("ENC2:!!!!"), None);
634
+ assert_eq!(envelope_version("{\"op\":1}"), None);
635
+ }
636
+
637
+ /// Adding `latest_snapshot` must not change the HMAC input of manifests that lack it:
638
+ /// a manifest written before the field existed still verifies after a parse/re-serialize.
639
+ #[test]
640
+ fn test_legacy_manifest_without_pointer_still_verifies() {
641
+ let mut legacy = new_encryption_manifest();
642
+ legacy.hmac = None;
643
+ legacy.hmac_salt = None;
644
+ // Legacy wire form: exactly the pre-pointer field set.
645
+ let legacy_bytes = format!(
646
+ r#"{{"algorithm":"{}","kdf":"{}","salt":"{}","kdf_params":{{"time_cost":{},"mem_cost":{},"parallelism":{}}},"key_version":0}}"#,
647
+ legacy.algorithm,
648
+ legacy.kdf,
649
+ legacy.salt,
650
+ legacy.kdf_params.time_cost,
651
+ legacy.kdf_params.mem_cost,
652
+ legacy.kdf_params.parallelism,
653
+ )
654
+ .into_bytes();
655
+ let (hmac, salt) = compute_manifest_hmac(&legacy_bytes, "pass").unwrap();
656
+
657
+ let parsed: EncryptionManifest = serde_json::from_slice(&legacy_bytes).unwrap();
658
+ assert!(parsed.latest_snapshot.is_none());
659
+ let reserialized = serde_json::to_vec(&parsed).unwrap();
660
+ assert_eq!(reserialized, legacy_bytes);
661
+ assert!(verify_manifest_integrity(&reserialized, &hmac, "pass", Some(&salt)).unwrap());
662
+
663
+ // With a pointer the bytes (and so the HMAC) change — the pointer is authenticated.
664
+ let mut pinned = parsed.clone();
665
+ pinned.latest_snapshot = Some(SnapshotPointer { file: "f".into(), mac: "m".into() });
666
+ let pinned_bytes = serde_json::to_vec(&pinned).unwrap();
667
+ assert!(!verify_manifest_integrity(&pinned_bytes, &hmac, "pass", Some(&salt)).unwrap());
668
+ }
591
669
  }
@@ -38,6 +38,12 @@ pub fn apply_op(
38
38
  "Remote device attempted to sync Private memory — rejected".into(),
39
39
  ));
40
40
  }
41
+ // The Muse export is local consent; a peer must never be able to add to it.
42
+ if memory.namespace.as_deref() == Some(crate::types::MUSE_EXPORT_NAMESPACE) {
43
+ return Err(CortexError::InvalidInput(
44
+ "Remote device attempted to write the reserved Muse export namespace — rejected".into(),
45
+ ));
46
+ }
41
47
 
42
48
  let entity_type = EntityType::Memory;
43
49
  let entity_id = memory.id;
@@ -323,6 +329,26 @@ mod tests {
323
329
  ts
324
330
  }
325
331
 
332
+ #[test]
333
+ fn test_peer_cannot_write_muse_export_namespace() {
334
+ use crate::types::{MemContent, MemObjectBuilder, MemSource, MemoryTier, PrivacyLevel, MUSE_EXPORT_NAMESPACE};
335
+ let s = SqliteStorage::open_in_memory().unwrap();
336
+ let idx = MemoryIndex::new();
337
+ let memory = MemObjectBuilder::new(MemoryTier::Semantic, MemContent::Text("injected".into()), MemSource::new("x"))
338
+ .privacy(PrivacyLevel::Public)
339
+ .namespace(MUSE_EXPORT_NAMESPACE)
340
+ .build();
341
+ let id = memory.id;
342
+ let op = SyncOp {
343
+ op_id: Uuid::new_v4(),
344
+ hlc: HlcTimestamp::new(100, 0, "dev-evil"),
345
+ payload: SyncPayload::MemoryUpsert { memory },
346
+ hmac: None,
347
+ };
348
+ assert!(apply_op(&op, &s, &idx).is_err());
349
+ assert!(s.get_memory(id).unwrap().is_none());
350
+ }
351
+
326
352
  #[test]
327
353
  fn test_belief_upsert_is_idempotent() {
328
354
  let s = SqliteStorage::open_in_memory().unwrap();