openlatch-client 0.6.19

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
//! The one definition of "a declared intercept host" (AGT-6 C-5, plan AGT-6-P2 D-8).
//!
//! A host is declared when it equals an exact host an agent lists, or when it matches one of the
//! fixed patterns OpenCode's binding lists: a fixed prefix, then exactly ONE DNS label
//! (`[a-z0-9-]{1,63}`), then a fixed suffix — `bedrock-runtime.<label>.amazonaws.com`,
//! `<label>.openai.azure.com`, `<label>-aiplatform.googleapis.com`. A pattern never matches an
//! empty label or a label carrying a dot, so it can never widen into a wildcard; patterns live in
//! code only, never in config.
//!
//! Every caller that decides "is this host ours" — the leaf minter and its CONNECT decision
//! (`ca::Interceptor`), attribution (`preflight::WiringState`) and the upstream-override guard —
//! goes through [`matches`], so the three can never disagree. No regex: "exactly one label" is a
//! byte test, and a byte test is what the corpus test below proves exhaustively (plan D-7).

use std::borrow::Cow;

/// One declared-host rule.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostRule {
    /// Exactly this host (lowercase, no trailing dot).
    Exact(&'static str),
    /// `<prefix><label><suffix>`, where `<label>` is exactly one DNS label ([`one_label`]).
    /// Both halves are lowercase; a suffix is matched whole, so `.amazonaws.com` and
    /// `.amazonaws.com.cn` are two rules and the first never swallows the second.
    OneLabel {
        prefix: &'static str,
        suffix: &'static str,
    },
}

impl HostRule {
    /// Whether this rule matches `host`, which must already be [`normalize`]d.
    fn matches_normalized(&self, host: &str) -> bool {
        match *self {
            HostRule::Exact(exact) => host == exact,
            HostRule::OneLabel { prefix, suffix } => host
                .strip_prefix(prefix)
                .and_then(|rest| rest.strip_suffix(suffix))
                .is_some_and(one_label),
        }
    }

    /// Whether this rule matches `host` (any case, an optional trailing dot).
    pub fn matches(&self, host: &str) -> bool {
        self.matches_normalized(&normalize(host))
    }

    /// True for a pattern rule, false for an exact host.
    pub fn is_pattern(&self) -> bool {
        matches!(self, HostRule::OneLabel { .. })
    }
}

/// Exactly one DNS label as C-5 defines it: 1 to 63 bytes of `[a-z0-9-]`. An empty label, a dot,
/// an uppercase letter (callers [`normalize`] first) and any non-ASCII byte are all refused.
pub fn one_label(label: &str) -> bool {
    (1..=63).contains(&label.len())
        && label
            .bytes()
            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
}

/// `host` lowercased with one trailing dot removed — the one shape every rule compares in.
/// Borrows when there is nothing to change (the CONNECT authority is already lowercase).
pub fn normalize(host: &str) -> Cow<'_, str> {
    let trimmed = host.strip_suffix('.').unwrap_or(host);
    if trimmed.bytes().any(|b| b.is_ascii_uppercase()) {
        Cow::Owned(trimmed.to_ascii_lowercase())
    } else {
        Cow::Borrowed(trimmed)
    }
}

/// Whether any rule in `rules` matches `host` (any case, an optional trailing dot).
pub fn matches(rules: &[HostRule], host: &str) -> bool {
    let host = normalize(host);
    rules.iter().any(|r| r.matches_normalized(&host))
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The six C-5 patterns, spelled as OpenCode's binding spells them. Kept here as well as in
    /// the binding so the corpus is a statement about the matcher, and a second test pins the
    /// binding's list to it.
    const PATTERNS: &[HostRule] = &[
        HostRule::OneLabel {
            prefix: "bedrock-runtime.",
            suffix: ".amazonaws.com",
        },
        HostRule::OneLabel {
            prefix: "bedrock-runtime.",
            suffix: ".amazonaws.com.cn",
        },
        HostRule::OneLabel {
            prefix: "",
            suffix: ".openai.azure.com",
        },
        HostRule::OneLabel {
            prefix: "",
            suffix: ".cognitiveservices.azure.com",
        },
        HostRule::OneLabel {
            prefix: "",
            suffix: ".services.ai.azure.com",
        },
        HostRule::OneLabel {
            prefix: "",
            suffix: "-aiplatform.googleapis.com",
        },
    ];

    /// A label of `len` copies of `c`.
    fn label(c: char, len: usize) -> String {
        std::iter::repeat_n(c, len).collect()
    }

    /// AC-13, plan D-7: every pattern × every label shape. A label is accepted iff it is one
    /// `[a-z0-9-]{1,63}` label once the host is normalized (lowercased, one trailing dot off).
    #[test]
    fn the_corpus_accepts_exactly_one_label_per_pattern() {
        // (label, accepted)
        let labels: Vec<(String, bool)> = vec![
            (label('a', 0), false),     // empty
            (label('a', 1), true),      // shortest
            (label('a', 63), true),     // longest
            (label('a', 64), false),    // one past the DNS limit
            ("us-east-1".into(), true), // a real region
            ("my-resource-01".into(), true),
            ("a.b".into(), false), // two labels
            ("us-east-1.evil".into(), false),
            (".".into(), false),
            ("a..b".into(), false),
            ("US-EAST-1".into(), true), // uppercase: DNS is case-insensitive, normalized first
            ("xn--bcher-kva".into(), true), // IDN in its ASCII form is one label
            ("bücher".into(), false),   // non-ASCII
            ("é".into(), false),
            ("a_b".into(), false), // underscore is not in the label alphabet
            ("a b".into(), false),
            ("*".into(), false), // a wildcard can never match
            ("a*".into(), false),
            ("a:443".into(), false), // a port is never part of the host
            ("a/b".into(), false),
        ];

        let mut checked = 0usize;
        for rule in PATTERNS {
            let HostRule::OneLabel { prefix, suffix } = *rule else {
                unreachable!("PATTERNS holds patterns only");
            };
            for (lbl, accepted) in &labels {
                let host = format!("{prefix}{lbl}{suffix}");
                assert_eq!(rule.matches(&host), *accepted, "{host:?} against {rule:?}");
                assert_eq!(matches(PATTERNS, &host), *accepted, "{host:?} in PATTERNS");
                // A trailing dot is the same host.
                let dotted = format!("{host}.");
                assert_eq!(
                    rule.matches(&dotted),
                    *accepted,
                    "{dotted:?} against {rule:?}"
                );
                // Uppercasing the whole host changes nothing either.
                let upper = host.to_uppercase();
                if upper.is_ascii() {
                    assert_eq!(
                        rule.matches(&upper),
                        *accepted,
                        "{upper:?} against {rule:?}"
                    );
                }
                checked += 1;
            }

            // Off-suffix and off-prefix forms of a valid label never match.
            let good = "us-east-1";
            let off = [
                format!("{prefix}{good}{suffix}.evil.test"),
                format!("{prefix}{good}{suffix}x"),
                format!("evil.{prefix}{good}{suffix}"),
                // The suffix with its leading separator dropped: the label runs into it.
                format!("{prefix}{good}{}", &suffix[1..]),
                suffix.trim_start_matches(['.', '-']).to_string(),
                suffix.to_string(),
                format!("{prefix}{suffix}"),
                format!("{prefix}{good}{suffix}.."),
                String::new(),
            ];
            for host in &off {
                assert!(!rule.matches(host), "{host:?} must not match {rule:?}");
            }
            // A character added inside the label keeps it one label: the control row.
            assert!(rule.matches(&format!("{prefix}{good}x{suffix}")));
            // A character before a non-empty prefix breaks the prefix.
            if !prefix.is_empty() {
                assert!(!rule.matches(&format!("x{prefix}{good}{suffix}")));
            }
        }
        assert_eq!(checked, PATTERNS.len() * labels.len());
    }

    /// `.amazonaws.com` never swallows `.amazonaws.com.cn`, and the reverse: each suffix is whole.
    #[test]
    fn a_suffix_is_matched_whole() {
        let com = PATTERNS[0];
        let cn = PATTERNS[1];
        assert!(com.matches("bedrock-runtime.us-east-1.amazonaws.com"));
        assert!(!com.matches("bedrock-runtime.cn-north-1.amazonaws.com.cn"));
        assert!(cn.matches("bedrock-runtime.cn-north-1.amazonaws.com.cn"));
        assert!(!cn.matches("bedrock-runtime.us-east-1.amazonaws.com"));
        // The Vertex pattern's label sits before a hyphen, not a dot.
        let vertex = PATTERNS[5];
        assert!(vertex.matches("us-central1-aiplatform.googleapis.com"));
        assert!(!vertex.matches("aiplatform.googleapis.com"));
        assert!(!vertex.matches("-aiplatform.googleapis.com"));
        assert!(!vertex.matches("a.us-central1-aiplatform.googleapis.com"));
        // Bedrock Mantle is a different host and stays out (Spec Non-Goals).
        assert!(!matches(PATTERNS, "bedrock-mantle.us-east-1.api.aws"));
    }

    #[test]
    fn exact_rules_compare_normalized() {
        let rules = [HostRule::Exact("api.groq.com")];
        assert!(matches(&rules, "api.groq.com"));
        assert!(matches(&rules, "API.Groq.COM"));
        assert!(matches(&rules, "api.groq.com."));
        assert!(!matches(&rules, "api.groq.com.."));
        assert!(!matches(&rules, "x.api.groq.com"));
        assert!(!matches(&rules, "api.groq.co"));
        assert!(!matches(&rules, ""));
        assert!(!HostRule::Exact("a.test").is_pattern());
        assert!(PATTERNS.iter().all(HostRule::is_pattern));
    }

    #[test]
    fn normalize_borrows_when_nothing_changes() {
        assert!(matches!(normalize("a.test"), Cow::Borrowed("a.test")));
        assert!(matches!(normalize("a.test."), Cow::Borrowed("a.test")));
        assert_eq!(normalize("A.Test."), "a.test");
        assert!(matches!(normalize("A.test"), Cow::Owned(_)));
    }

    #[test]
    fn one_label_is_the_dns_label_alphabet_and_length() {
        assert!(!one_label(""));
        assert!(one_label("a"));
        assert!(one_label(&label('z', 63)));
        assert!(!one_label(&label('z', 64)));
        assert!(one_label("0-9"));
        assert!(!one_label("A"));
        assert!(!one_label("a.b"));
        assert!(!one_label("ä"));
    }
}