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.
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/Cargo.lock +9 -5
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/PKG-INFO +1 -1
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/Cargo.toml +2 -1
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/export.rs +7 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/lib.rs +34 -3
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/crypto.rs +78 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/merge.rs +26 -0
- cortex_ai_memory-2.4.0/cortex-core/src/sync/mod.rs +1211 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/oplog.rs +13 -4
- cortex_ai_memory-2.4.0/cortex-core/src/sync/snapshot.rs +717 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/state.rs +125 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/types.rs +5 -0
- cortex_ai_memory-2.4.0/cortex-core/tests/test_reserved_namespace.rs +112 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_sync.rs +1 -1
- cortex_ai_memory-2.4.0/cortex-core/tests/test_sync_hardening.rs +554 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-python/Cargo.toml +2 -1
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/pyproject.toml +1 -1
- cortex_ai_memory-2.2.1/cortex-core/src/sync/mod.rs +0 -687
- cortex_ai_memory-2.2.1/cortex-core/src/sync/snapshot.rs +0 -361
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/Cargo.toml +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/README.md +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/benches/comparison_bench.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/benches/memory_index_bench.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/belief.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/compression.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/consolidation.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/context.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/embedder.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/episode.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/events.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/inference.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/people.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugin_manager.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/mod.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/plugins/tag_classifier.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/procedural.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/relationship.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/retrieval.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/semantic.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/memory_index.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/mod.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/sqlite.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/storage/traits.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/hlc.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/provider.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/secret.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/sync/watcher.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/src/working.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_belief.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cache_and_perf.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_compression_advanced.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_consolidation_advanced.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_context.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_contradiction_flow.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_e2e.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_cortex_full.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_episode.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_events.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_full_privacy_chain.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_gdrive_real.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_ingest_parity.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_memory_index.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_optimizations.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_people.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_plugin_manager_ext.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_privacy_sync.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_procedural.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_retrieval_advanced.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_semantic.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_storage_advanced.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_temporal_retrieval.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_traits_defaults.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_types.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-core/tests/test_working_memory.rs +0 -0
- {cortex_ai_memory-2.2.1 → cortex_ai_memory-2.4.0}/cortex-python/README.md +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -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
|
-
|
|
808
|
-
|
|
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();
|