cultnet-rs: stored envelope payloads decode through cultcache-rs decode_payload

replication's document_record_from_envelope and the service identity credential read no longer
call rmp_serde::from_slice on a stored payload, so the 256 bound holds there too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 17:52:25 +02:00
commit d47cd03890
3 changed files with 74 additions and 2 deletions

View file

@ -522,7 +522,7 @@ impl CultNetDocumentRegistry {
envelope: &CultCacheEnvelope,
) -> Result<CultNetDocumentRecord<Value>> {
let binding = self.require_binding(&envelope.r#type)?;
let payload: Value = rmp_serde::from_slice(&envelope.payload).with_context(|| {
let payload: Value = cultcache_rs::decode_payload(&envelope.payload).with_context(|| {
format!(
"failed to decode CultCache envelope {:?} at {:?} as generic CultNet payload",
envelope.r#type, envelope.key

View file

@ -306,7 +306,7 @@ fn open_service_identity_envelopes<P: ServiceIdentityProfile>(
{
bail!("service identity store belongs to a different profile or schema");
}
let entry: ServiceIdentityPrivateEntry = rmp_serde::from_slice(&envelope.payload)
let entry: ServiceIdentityPrivateEntry = cultcache_rs::decode_payload(&envelope.payload)
.context("service identity private payload is malformed MessagePack")?;
validate_private::<P>(&entry)?;
if entry.protector_kind != platform_protector_kind() || entry.assurance != platform_assurance()
@ -741,6 +741,34 @@ mod tests {
Ok(())
}
#[test]
fn a_credential_payload_nested_past_the_bound_is_refused_by_the_cache_walk() -> Result<()> {
let limit = cultcache_rs::MAX_PAYLOAD_DEPTH;
let credential = |levels: usize| -> Result<Vec<u8>> {
let envelope = CultCacheEnvelope {
key: IdunnServiceIdentity::PRIVATE_KEY.into(),
r#type: IdunnServiceIdentity::PRIVATE_TYPE.into(),
payload: [vec![0x91u8; levels], vec![0xc0]].concat(),
stored_at: "2026-10-11T00:00:00Z".into(),
schema_id: Some(IdunnServiceIdentity::PRIVATE_SCHEMA.into()),
};
Ok(rmp_serde::to_vec(&vec![envelope])?)
};
let past = open_service_identity_credential_bytes::<IdunnServiceIdentity>(&credential(limit + 1)?)
.err()
.expect("a payload past the bound is refused");
assert_eq!(
past.downcast_ref::<cultcache_rs::PayloadTooDeep>(),
Some(&cultcache_rs::PayloadTooDeep { depth: limit + 1, limit })
);
// At the bound the walk lets the payload through to the decoder, which refuses its shape.
let at = open_service_identity_credential_bytes::<IdunnServiceIdentity>(&credential(limit)?)
.err()
.expect("the payload is not a credential");
assert!(at.downcast_ref::<cultcache_rs::PayloadTooDeep>().is_none());
Ok(())
}
#[test]
fn immutable_credential_reader_opens_without_a_path_and_retains_signing_authority() -> Result<()>
{

View file

@ -1,6 +1,7 @@
use anyhow::Result;
use chrono::Duration;
use chrono::Utc;
use cultcache_rs::CacheBackingStore;
use cultcache_rs::CultCache;
use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
@ -1778,6 +1779,49 @@ fn document_registry_advertises_mutation_contracts_with_bindings() -> Result<()>
Ok(())
}
#[test]
fn a_stored_payload_nested_past_the_bound_is_not_replicated_as_a_document() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("foreign.msgpack");
let limit = cultcache_rs::MAX_PAYLOAD_DEPTH;
let envelope = |key: &str, levels: usize| cultcache_rs::CultCacheEnvelope {
key: key.to_string(),
r#type: GhostlightAgentStateFixture::TYPE.to_string(),
payload: [vec![0x91u8; levels], vec![0xc0]].concat(),
stored_at: "2026-10-11T00:00:00Z".to_string(),
schema_id: None,
};
let mut registry = CultNetDocumentRegistry::new();
registry.register(CultNetDocumentBinding::for_entry::<GhostlightAgentStateFixture>(
"ghostlight.agent_state.v0".to_string(),
));
// A store written by something that does not hold the bound.
let mut backing = SingleFileMessagePackBackingStore::new(&store);
backing.push(&envelope("at", limit))?;
let mut cache = CultCache::new();
cache.register_entry_type::<GhostlightAgentStateFixture>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
cache.pull_all_backing_stores()?;
let at_limit = registry.create_snapshot_response(&cache, "at", None, None)?;
let CultNetMessage::SnapshotResponse { documents, .. } = at_limit else {
panic!("expected a snapshot response");
};
assert_eq!(documents.len(), 1);
backing.push(&envelope("past", limit + 1))?;
let mut cache = CultCache::new();
cache.register_entry_type::<GhostlightAgentStateFixture>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
cache.pull_all_backing_stores()?;
let error = registry.create_snapshot_response(&cache, "past", None, None).unwrap_err();
assert_eq!(
error.downcast_ref::<cultcache_rs::PayloadTooDeep>(),
Some(&cultcache_rs::PayloadTooDeep { depth: limit + 1, limit })
);
Ok(())
}
#[test]
fn document_registry_replicates_typed_cultcache_state() -> Result<()> {
let temp = tempfile::tempdir()?;