Skip to main content

openmoat_core/
ir.rs

1//! The policy compiler's intermediate representation (ADR-019).
2//!
3//! [`lower()`] derives from `policy.yaml` the part of a decision an operating
4//! system layer can enforce: which paths may be read or written and which
5//! hosts may be reached. Every OS backend (a host's sandbox settings, Seatbelt,
6//! Landlock, the egress proxy) is generated from this one [`Enforcement`]; the
7//! hook backend is the engine itself ([`crate::CompiledPolicy`]), compiled from
8//! the same policy and context.
9//!
10//! An OS layer can only allow or deny, so lowering never widens: a hook `ask`
11//! becomes a deny, and whatever cannot be represented exactly is narrowed and
12//! recorded as a [`Loss`]. The IR's verdict on any read, write or host is
13//! therefore the engine's verdict with `ask` read as `deny`, or stricter.
14
15use std::fmt;
16
17use serde::Serialize;
18
19use crate::kind::Kind;
20
21mod check;
22mod lower;
23
24pub use check::Checker;
25pub use lower::{CLOUD_METADATA, CLOUD_METADATA_RULE, READ_ROOTS_RULE, lower};
26
27/// What an OS layer may enforce for one policy in one context.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct Enforcement {
30    /// File reads and writes.
31    pub fs: Filesystem,
32    /// Outbound network.
33    pub egress: Egress,
34    /// Secrets the egress proxy and Claude Code's own broker inject for the
35    /// agent (ADR-020). One entry per policy `secrets:` item; OS backends
36    /// consume these without reading the policy again.
37    pub secrets: Vec<BrokeredSecret>,
38    /// Limits on the agent's processes. Always empty: no policy key sets one yet.
39    pub limits: Vec<ProcessLimit>,
40    /// Rules only the hook can apply, by kind. Shell semantics, environment
41    /// variables, MCP tool names and executable pins name things an OS layer
42    /// cannot see (a command line, a variable, a tool), so they are not lowered
43    /// and stay with the hook decision (ADR-018).
44    pub decide_only: Vec<DecideOnly>,
45    /// Where the IR is stricter than the hook decision, with the reason.
46    pub losses: Vec<Loss>,
47    /// Where OS layers may be wider than the hook decision, listed in the
48    /// policy (`sandbox.read_roots`). The hook keeps deciding these accesses;
49    /// deny rules still win over them. Not part of [`Filesystem`], so the IR's
50    /// own verdicts stay the hook's or stricter.
51    pub allowances: Vec<Allowance>,
52}
53
54/// File access rules. Paths are absolute, slash-separated globs with `~` and
55/// `${project}` already expanded once per spelling of each root (the home
56/// directory and project as given and with their symlinks resolved).
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58pub struct Filesystem {
59    /// Whether paths compare case-insensitively (default macOS and Windows file systems).
60    pub case_insensitive: bool,
61    /// Reads.
62    pub read: Access,
63    /// Writes, creates, deletes and moves.
64    pub write: Access,
65}
66
67/// Network rules. Hosts are lowercase globs and always compare case-insensitively.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
69pub struct Egress {
70    /// Any connection, a host fetch tool's included.
71    pub net: Access,
72    /// Requests by a host's read-only fetch tool (`WebFetch`), which `fetch`
73    /// and `net` rules both govern (ADR-017).
74    pub fetch: Access,
75}
76
77/// The rules for one kind of access, applied in order: a matching `deny` rule
78/// denies; otherwise a matching `allow` rule allows; otherwise `default`.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
80pub struct Access {
81    /// The effect when no rule matches.
82    pub default: Effect,
83    /// Rules that deny; an allow never overrides them.
84    pub deny: Vec<Rule>,
85    /// Rules that allow.
86    pub allow: Vec<Rule>,
87}
88
89/// One policy rule group's patterns from one list. It matches when at least
90/// one pattern matches and no `!` exclusion does, as in the policy.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub struct Rule {
93    /// The policy rule group's id ([`CLOUD_METADATA_RULE`] for the built-in one).
94    pub id: String,
95    /// The policy list the patterns come from (`net` patterns also govern `fetch`).
96    pub list: Kind,
97    /// Expanded globs; a leading `!` excludes.
98    pub patterns: Vec<String>,
99}
100
101/// What an OS layer does with an access.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
103#[serde(rename_all = "lowercase")]
104pub enum Effect {
105    /// The access may proceed.
106    Allow,
107    /// The access is blocked.
108    Deny,
109}
110
111/// The policy rules of one kind that only the hook applies.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
113pub struct DecideOnly {
114    /// `shell`, `env.read`, `env.set`, `mcp` or `executables`.
115    pub kind: String,
116    /// Rule group ids, or program names for `executables`.
117    pub rules: Vec<String>,
118}
119
120/// A place where the IR denies something the hook would not deny.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122pub struct Loss {
123    /// The access the loss applies to.
124    pub kind: Kind,
125    /// The rule group id, or `default.<kind>`/`default` for the defaults table.
126    pub rule: String,
127    /// What the hook does and what OS layers do instead.
128    pub message: String,
129}
130
131impl fmt::Display for Loss {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{} `{}`: {}", self.kind, self.rule, self.message)
134    }
135}
136
137/// Accesses OS layers allow although the hook would not (ADR-019: the only
138/// way an OS layer may be wider than the hook, and always listed).
139#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
140pub struct Allowance {
141    /// The access the allowance applies to.
142    pub kind: Kind,
143    /// Where it comes from (`sandbox.read_roots`).
144    pub rule: String,
145    /// Expanded globs: each root and everything below it.
146    pub patterns: Vec<String>,
147    /// What the hook does and what OS layers do instead.
148    pub message: String,
149}
150
151impl fmt::Display for Allowance {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "{} `{}`: {}", self.kind, self.rule, self.message)
154    }
155}
156
157/// A secret the egress proxy or Claude Code's own broker injects (ADR-020).
158/// A copy of [`crate::Secret`]: the IR carries only the fields OS backends
159/// need, so a backend can read `ir.secrets` without the policy. Its `Debug`
160/// output names only the id, as for [`crate::Secret`].
161#[derive(Clone, PartialEq, Eq, Serialize)]
162pub struct BrokeredSecret {
163    /// Stable identifier (lowercase letters, digits and `-`).
164    pub id: String,
165    /// The one host whose requests receive the secret, lowercase DNS or IPv4.
166    pub host: String,
167    /// The request header the secret goes in (`Authorization`, `X-Api-Key`).
168    pub header: String,
169    /// Where OpenMoat reads the value.
170    pub source: crate::SecretSource,
171    /// Also inject into plain-HTTP requests; off unless the policy sets it.
172    pub plain_http: bool,
173}
174
175impl std::fmt::Debug for BrokeredSecret {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.debug_struct("BrokeredSecret")
178            .field("id", &self.id)
179            .finish_non_exhaustive()
180    }
181}
182
183/// A process limit. No value exists until the policy schema defines one.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
185pub enum ProcessLimit {}
186
187#[cfg(test)]
188mod tests;