Skip to main content

mur_common/
authz.rs

1//! The one spelling of "you may not" that every authorization gate uses
2//! (spec 2026-09-12 execution-limits §3.8, D9). The gates stay where they
3//! are; this is how their refusals are recognised across a process boundary
4//! — an MCP server's `isError` text, a tool's error — so the loop can treat
5//! them as terminal for the tool instead of as something to retry.
6
7pub const NOT_AUTHORIZED_PREFIX: &str = "not authorized:";
8
9/// `not authorized: <msg>` — the message every gate emits.
10pub fn not_authorized(msg: &str) -> String {
11    format!("{NOT_AUTHORIZED_PREFIX} {msg}")
12}
13
14/// Does this text (possibly wrapped by an MCP server as `Error: …`) carry a
15/// refusal? Prefix only — a tool that merely mentions authorization in its
16/// output is not refusing.
17pub fn is_not_authorized(text: &str) -> bool {
18    let t = text.trim_start();
19    let t = t.strip_prefix("Error:").map(str::trim_start).unwrap_or(t);
20    t.len() >= NOT_AUTHORIZED_PREFIX.len()
21        && t[..NOT_AUTHORIZED_PREFIX.len()].eq_ignore_ascii_case(NOT_AUTHORIZED_PREFIX)
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[test]
29    fn refusals_are_recognised_with_or_without_the_mcp_wrapper() {
30        assert!(is_not_authorized(&not_authorized(
31            "target 'ghost' for parallel_jobs"
32        )));
33        assert!(is_not_authorized("Error: not authorized: fleet_run denied"));
34        assert!(is_not_authorized("  NOT AUTHORIZED: x"));
35        assert!(
36            !is_not_authorized("the file says: not authorized: nope"),
37            "prefix, not substring"
38        );
39        assert!(!is_not_authorized("permission denied"));
40    }
41}