Skip to main content

kmp_application/memory/
ref_boundary.rs

1/// Reject ref spellings that can escape the graph namespace or become
2/// ambiguous when rendered, logged, or used as storage keys.
3pub fn validate_ref_token(path: &str, value: &str) -> Result<(), String> {
4    let unsafe_character = value
5        .chars()
6        .any(|ch| ch.is_control() || ch.is_whitespace() || matches!(ch, '/' | '\\'));
7    let unsafe_segment = value
8        .split(':')
9        .any(|segment| segment.is_empty() || matches!(segment, "." | ".."));
10    if unsafe_character || unsafe_segment {
11        return Err(format!(
12            "invalid `{path}` `{value}`; memory refs cannot contain whitespace, control characters, path separators, or empty/dot path segments"
13        ));
14    }
15    Ok(())
16}
17
18/// Entry ids are strict descendants of the about they mutate. The about
19/// anchor itself is deliberately excluded: ingesting an entry must never
20/// change the root node's kind or payload.
21pub fn validate_supplied_entry_ref(about: &str, path: &str, entry_ref: &str) -> Result<(), String> {
22    validate_ref_token(path, entry_ref)?;
23    let owned_prefix = format!("{about}:");
24    if !entry_ref.starts_with(&owned_prefix) {
25        return Err(format!(
26            "`{path}` `{entry_ref}` does not belong to about `{about}`; it must start with `{owned_prefix}` and cannot replace the about anchor or a node from another about. Omit {path} to generate a safe ref for a new memory"
27        ));
28    }
29    Ok(())
30}
31
32/// Evidence nodes use a distinct prefix, but remain owned by exactly one
33/// about through the entry ref embedded after `evidence:`.
34pub fn validate_supplied_evidence_ref(
35    about: &str,
36    path: &str,
37    evidence_ref: &str,
38) -> Result<(), String> {
39    validate_ref_token(path, evidence_ref)?;
40    let owned_prefix = format!("evidence:{about}:");
41    if !evidence_ref.starts_with(&owned_prefix) {
42        return Err(format!(
43            "`{path}` `{evidence_ref}` does not belong to about `{about}`; evidence ids must start with `{owned_prefix}`"
44        ));
45    }
46    Ok(())
47}
48
49/// A relation-like ref may name the about anchor, one of its entry nodes, an
50/// owned evidence node, or the canonical namespace used for its dimensions.
51pub fn validate_supplied_member_ref(
52    about: &str,
53    path: &str,
54    member_ref: &str,
55) -> Result<(), String> {
56    validate_ref_token(path, member_ref)?;
57    let entry_prefix = format!("{about}:");
58    let evidence_prefix = format!("evidence:{about}:");
59    let dimension_prefix = format!("about:{about}:dimension:");
60    if member_ref != about
61        && !member_ref.starts_with(&entry_prefix)
62        && !member_ref.starts_with(&evidence_prefix)
63        && !member_ref.starts_with(&dimension_prefix)
64    {
65        return Err(format!(
66            "`{path}` `{member_ref}` does not belong to about `{about}`"
67        ));
68    }
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::{
75        validate_ref_token, validate_supplied_entry_ref, validate_supplied_evidence_ref,
76        validate_supplied_member_ref,
77    };
78
79    const ABOUT: &str = "incident:alfa";
80    const HOSTILE_REFS: &[&str] = &[
81        "incident:gamma:entry:observation:foreign",
82        "incident:beta",
83        "incident:alfa:entry:x\nincident:beta:entry:y",
84        "../../incident:beta:entry:x",
85    ];
86
87    #[test]
88    fn every_owned_ref_validator_rejects_the_shared_hostile_vectors() {
89        for hostile in HOSTILE_REFS {
90            assert!(
91                validate_supplied_entry_ref(ABOUT, "entry", hostile).is_err(),
92                "entry validator accepted {hostile:?}"
93            );
94            assert!(
95                validate_supplied_evidence_ref(ABOUT, "evidence", hostile).is_err(),
96                "evidence validator accepted {hostile:?}"
97            );
98            assert!(
99                validate_supplied_member_ref(ABOUT, "member", hostile).is_err(),
100                "member validator accepted {hostile:?}"
101            );
102        }
103    }
104
105    #[test]
106    fn owned_graph_namespaces_remain_valid() {
107        validate_ref_token("about", ABOUT).expect("safe about");
108        validate_supplied_entry_ref(ABOUT, "entry", "incident:alfa:entry:decision:one")
109            .expect("owned entry");
110        validate_supplied_evidence_ref(
111            ABOUT,
112            "evidence",
113            "evidence:incident:alfa:entry:decision:one:current",
114        )
115        .expect("owned evidence");
116        for member in [
117            ABOUT,
118            "incident:alfa:entry:decision:one",
119            "evidence:incident:alfa:entry:decision:one:current",
120            "about:incident:alfa:dimension:agentic_process:run",
121        ] {
122            validate_supplied_member_ref(ABOUT, "member", member).expect("owned member");
123        }
124    }
125}