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;