didbot_policy_records/builtin.rs
1//! Source 1: policies compiled into the binary, immutable.
2//!
3//! `plan/policy.md`: "Things unsafe to admit into public existence. An
4//! application that does not want agents using it opens a pull request
5//! against didbot asking to be denied as a client; that denial ships in the
6//! binary." This is where that PR lands: [`builtin_policies`] returns a
7//! fixed `Vec`, and the only way to change what it contains is to edit
8//! `floor/` and ship a new build. There is no config path, no record, no
9//! admin surface that can add to or remove from this list at runtime --
10//! that is the whole point of "immutable."
11//!
12//! A built-in applies to every account on every deployment and cannot be
13//! turned off. So it is only for a rule no operator would ever want an
14//! agent to break. Anything an operator might reasonably want differently
15//! is the operator's own record.
16//!
17//! # A built-in is a record
18//!
19//! Each one is the `bot.did.policy` record an operator would write, kept
20//! in `floor/` beside this crate and read at startup by the same
21//! [`crate::records`] reader, split into statements by the same
22//! [`crate::set::declarations`], and compiled by the same
23//! [`crate::compile`] against the same engines. So a built-in can be copied
24//! into an operator's repository unchanged, the refusal an agent reads is
25//! the statement's own reason, it reports on a dashboard the way an
26//! operator's record does, and a change to an engine that breaks a built-in
27//! breaks this crate's tests rather than a deployment. `floor/README.md`
28//! says what each one is for and where its numbers come from, and
29//! [`builtin_records`] hands the records out for a dashboard or a test to
30//! read.
31//!
32//! What a built-in gets that an operator's record does not is its
33//! applicability: a record from the operator is routed to every write and
34//! filters on the collection inside the evaluator, while a built-in names
35//! the exact collections it reaches, so a write anywhere else never pays
36//! for it. That is `plan/policy.md`'s "a policy gating common writes should
37//! be cheap and in-process", applied to rules every write on every server
38//! would otherwise consult. A built-in that reaches every collection, such
39//! as a client denial, says so with `Scope::Any`.
40
41use didbot_policy::{Applicability, Scope, Staleness};
42use serde_json::Value;
43
44use crate::record::ParsedPolicy;
45use crate::records::PolicyRecord;
46
47/// One statement of one policy compiled into the binary, under the id a
48/// report calls it by: `builtin/{name}/{statement}/{action}`, the same
49/// `{key}/{statement}/{action}` shape an operator's record is registered
50/// under, with the source in place of a record key.
51#[derive(Clone)]
52pub struct BuiltinPolicy {
53 /// What a report calls this policy.
54 pub name: String,
55 /// The declaration [`crate::set::declarations`] made of it, which
56 /// [`crate::merge::build`] compiles exactly as it compiles an
57 /// operator's.
58 pub policy: ParsedPolicy,
59}
60
61/// A builtin's nominal revision: `0`, always. A builtin is not an
62/// observation of anything -- it ships with the binary and has no author
63/// writing successive edits for [`didbot_policy::Revision`] to order, so
64/// there is nothing for this value to compare against. It exists only
65/// because [`didbot_policy::PolicyDeclaration`] always carries one, for
66/// `plan/policy.md`'s "record which policy version admitted each write".
67pub const BUILTIN_REVISION: didbot_policy::Revision = didbot_policy::Revision(0);
68
69/// The `createdAt` a built-in's record carries when its file leaves it
70/// out, the way the recommended catalogue's files do.
71///
72/// The field is the record's, and a built-in has no author writing it on a
73/// date, so nothing reads it; the reader requires one.
74const WRITTEN_AT: &str = "1970-01-01T00:00:00.000Z";
75
76/// One built-in: its record as written, the collections it reaches, and
77/// how stale its evaluator's view may be.
78struct Builtin {
79 record: Value,
80 collections: Scope<&'static str>,
81 staleness: Staleness,
82}
83
84/// The record `source` holds.
85fn record(source: &str) -> Value {
86 serde_json::from_str(source).expect("a built-in record is JSON")
87}
88
89/// The built-in `record` is, reaching `collections`.
90fn builtin(mut record: Value, collections: Scope<&'static str>) -> Builtin {
91 if record.get("createdAt").is_none() {
92 record["createdAt"] = Value::String(WRITTEN_AT.to_owned());
93 }
94 Builtin {
95 record,
96 collections,
97 // Every floor engine is deterministic, exact and in process, and
98 // the counting one routes every write on the server, so none of
99 // them may answer from a view that has fallen behind.
100 staleness: Staleness::Linearized,
101 }
102}
103
104/// The built-ins this binary ships. See `ships_exactly_these_builtins` for
105/// the list as a reviewer reads it, and `floor/README.md` for what each one
106/// is for.
107fn builtins() -> Vec<Builtin> {
108 vec![
109 builtin(
110 record(DECLARED_AI_PREFERENCE),
111 // Every collection: a record in any of them can carry somebody's
112 // DID, and a list of the ones that do today is a list that stops
113 // being true the next time a lexicon gains a field.
114 Scope::Any,
115 ),
116 builtin(
117 record(include_str!("../floor/keep-bot-label.json")),
118 Scope::Only(vec!["app.bsky.actor.profile"]),
119 ),
120 builtin(record(include_str!("../floor/write-rate.json")), Scope::Any),
121 builtin(
122 record(include_str!("../floor/follow-rate.json")),
123 Scope::Only(vec!["app.bsky.graph.follow"]),
124 ),
125 builtin(
126 record(include_str!("../floor/contact-rate.json")),
127 Scope::Only(vec![
128 "app.bsky.feed.like",
129 "app.bsky.feed.post",
130 "app.bsky.feed.repost",
131 ]),
132 ),
133 ]
134}
135
136/// The floor rule that honours a declared AI preference. See
137/// `plan/ai-preference.md`.
138pub const DECLARED_AI_PREFERENCE: &str = include_str!("../floor/declared-ai-preference.json");
139
140/// [`builtins`], plus the records this crate's own tests exercise the
141/// machinery with. Those records exist only in the test build.
142fn loaded() -> Vec<Builtin> {
143 #[cfg_attr(not(test), allow(unused_mut))]
144 let mut loaded = builtins();
145 #[cfg(test)]
146 loaded.extend(tests::examples());
147 loaded
148}
149
150/// Every built-in, as the `bot.did.policy` record it is written as.
151#[must_use]
152pub fn builtin_records() -> Vec<Value> {
153 loaded().into_iter().map(|builtin| builtin.record).collect()
154}
155
156/// Every policy this build ships compiled in. Fixed at compile time -- see
157/// this module's doc comment.
158///
159/// One [`BuiltinPolicy`] per statement per action the statement can decide,
160/// exactly as an operator's record is registered, because it is the same
161/// code that splits it.
162///
163/// # Adding one
164///
165/// Write the record in `floor/`, with its cases beside it, and add it to
166/// `builtins` with the collections it reaches. A PR asking to be denied as
167/// a client is a `denyClient` statement under `oauth.authorize`,
168/// `oauth.token` and `record.write`, reaching `Scope::Any` collections, so
169/// the application is refused at sign-in and at token issue, and a token it
170/// already holds is refused at the write. Then change
171/// `ships_exactly_these_builtins`: adding one is its own reviewed change.
172///
173/// # Panics
174///
175/// If a built-in's record does not read, or names an action this server
176/// does not answer. That is a defect in `floor/`, not a runtime condition:
177/// this crate's tests read every one of them. Whether a record's
178/// *predicate* compiles is the engine's answer, made where an operator's
179/// record gets it -- `crate::compile` -- and `the_floor_compiles` is what
180/// holds the floor to compiling cleanly.
181#[must_use]
182pub fn builtin_policies() -> Vec<BuiltinPolicy> {
183 let mut policies = Vec::new();
184 for builtin in loaded() {
185 let record = PolicyRecord::from_json(&builtin.record)
186 .unwrap_or_else(|error| panic!("a built-in record does not read: {error}"));
187 let name = record.display_name("builtin");
188 let declared = crate::set::declarations(&format!("builtin/{name}"), &record)
189 .unwrap_or_else(|error| panic!("built-in {name}: {error}"));
190 for (id, mut policy) in declared.policies {
191 narrow(&mut policy.applicability, &builtin.collections);
192 narrow(&mut policy.observes, &builtin.collections);
193 policy.staleness = builtin.staleness;
194 policy.revision = BUILTIN_REVISION;
195 policies.push(BuiltinPolicy { name: id, policy });
196 }
197 }
198 policies
199}
200
201/// Narrows a write's applicability to the collections a built-in reaches,
202/// so the tree routes it nowhere else. Leaves every other subject shape
203/// alone: a client denial at sign-in has no collection to narrow.
204fn narrow(applicability: &mut Applicability, only: &Scope<&'static str>) {
205 if let (Applicability::Write { collections, .. }, Scope::Only(named)) = (applicability, only) {
206 *collections = Scope::Only(named.iter().map(|name| (*name).to_owned()).collect());
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::engine::build_tree;
214 use crate::LastGoodPolicies;
215 use didbot_policy::{
216 AccountKind, Change, ClientId, DeploymentAttributes, Diff, PolicyTree, Subject,
217 SubjectAttributes, SubjectKind, Universal, Verdict, WriteAction,
218 };
219 use serde_json::json;
220 use std::collections::HashMap;
221
222 /// The reason the example built-in refuses with.
223 const EXAMPLE_REASON: &str = "app.example.record is refused by the test build's built-in.";
224
225 /// The reason the example client denial refuses with.
226 const CLIENT_REASON: &str = "client.example is refused by the test build's built-in.";
227
228 /// Every built-in this module adds to [`loaded`].
229 pub(super) fn examples() -> Vec<Builtin> {
230 vec![example(), example_client()]
231 }
232
233 /// The record this module exercises the machinery with. Its statement
234 /// matches every `app.example.*` collection, but it names only
235 /// `app.example.record`: a write to `app.example.other` is admitted
236 /// because routing never reaches it, not because the regex declined.
237 fn example() -> Builtin {
238 builtin(
239 json!({
240 "$type": "bot.did.policy",
241 "name": "example",
242 "description": "Refuses every write to app.example.record.",
243 "actions": ["record.write"],
244 "document": {
245 "$type": "bot.did.policy#regex",
246 "statements": [{
247 "kind": "denyPathValueContaining",
248 "collection": r"^app\.example\.",
249 "path": "",
250 "denied": [{"regex": ""}],
251 "reason": EXAMPLE_REASON,
252 }],
253 },
254 }),
255 Scope::Only(vec!["app.example.record"]),
256 )
257 }
258
259 /// A client denial at sign-in, at token issue and at the write. It
260 /// reaches every collection.
261 fn example_client() -> Builtin {
262 builtin(
263 json!({
264 "$type": "bot.did.policy",
265 "name": "example-client",
266 "description": "Refuses client.example everywhere.",
267 "actions": ["oauth.authorize", "oauth.token", "record.write"],
268 "document": {
269 "$type": "bot.did.policy#regex",
270 "statements": [{
271 "kind": "denyClient",
272 "clientId": r"^https://client\.example/",
273 "reason": CLIENT_REASON,
274 }],
275 },
276 }),
277 Scope::Any,
278 )
279 }
280
281 /// A tree holding the loaded built-ins and nothing else, built the way
282 /// a deployment builds one.
283 fn tree() -> PolicyTree {
284 let (tree, report) = build_tree(
285 builtin_policies(),
286 &HashMap::new(),
287 &HashMap::new(),
288 &LastGoodPolicies::new(),
289 );
290 assert!(
291 report.scoped_denials.is_empty(),
292 "a built-in that does not compile denies everything it covers: {:?}",
293 report.scoped_denials
294 );
295 assert!(report
296 .registered
297 .contains_key("builtin/example/0/record.write"));
298 tree
299 }
300
301 fn attributes() -> SubjectAttributes<'static> {
302 SubjectAttributes {
303 kind: SubjectKind::Known(AccountKind::Agent),
304 handle: Some("agent.pds.example"),
305 depth: None,
306 }
307 }
308
309 fn universal() -> Universal<'static> {
310 Universal {
311 deployment: DeploymentAttributes {
312 pds_did: "did:web:pds.example",
313 pds_hostname: "pds.example",
314 operator_did: None,
315 },
316 now: time::OffsetDateTime::UNIX_EPOCH,
317 }
318 }
319
320 fn create<'a>(
321 client_id: Option<&'a ClientId>,
322 collection: &'a str,
323 changes: &'a [Change<'a>],
324 ) -> Subject<'a> {
325 Subject::Write {
326 account: "did:web:agent.pds.example",
327 attributes: attributes(),
328 universal: universal(),
329 client_id,
330 collection,
331 action: WriteAction::Create,
332 diff: Diff { changes },
333 before: None,
334 after: None,
335 lookups: &[],
336 }
337 }
338
339 /// Adding a built-in ships it to every account on every deployment, so
340 /// it is a deliberate change reviewed on its own, and this list is the
341 /// line it has to edit.
342 #[test]
343 fn ships_exactly_these_builtins() {
344 let mut ids: Vec<String> = builtins()
345 .into_iter()
346 .flat_map(|builtin| {
347 let record = PolicyRecord::from_json(&builtin.record).expect("reads");
348 let name = record.display_name("builtin");
349 crate::set::declarations(&format!("builtin/{name}"), &record)
350 .expect("declares")
351 .policies
352 .into_iter()
353 .map(|(id, _)| id)
354 })
355 .collect();
356 ids.sort();
357 assert_eq!(
358 ids,
359 [
360 "builtin/contact-rate/0/record.write",
361 "builtin/contact-rate/1/record.write",
362 "builtin/declared-ai-preference/0/record.write",
363 "builtin/follow-rate/0/record.write",
364 "builtin/follow-rate/1/record.write",
365 "builtin/keep-bot-label/keep-bot-label/record.write",
366 "builtin/keep-bot-label/keep-the-profile/record.write",
367 "builtin/write-rate/0/record.write",
368 "builtin/write-rate/1/record.write",
369 "builtin/write-rate/2/record.write",
370 "builtin/write-rate/3/record.write",
371 ]
372 );
373 }
374
375 /// Every record in `floor/` compiles, for the engine it names, on the
376 /// build that ships it. A built-in that did not would deny everything
377 /// it covers instead of what it says.
378 #[test]
379 fn the_floor_compiles() {
380 let _ = tree();
381 }
382
383 #[test]
384 fn a_builtin_compiles_and_routes_by_its_collections() {
385 let policies = builtin_policies();
386 let policy = policies
387 .iter()
388 .find(|policy| policy.name == "builtin/example/0/record.write")
389 .expect("the example is registered under its name");
390 match &policy.policy.applicability {
391 Applicability::Write {
392 collections: Scope::Only(collections),
393 ..
394 } => assert_eq!(
395 collections.iter().collect::<Vec<_>>(),
396 ["app.example.record"]
397 ),
398 other => panic!("expected a write scoped to its collections, got {other:?}"),
399 }
400 assert!(builtin_records()
401 .iter()
402 .any(|record| record["name"] == "example"));
403 }
404
405 #[test]
406 fn a_builtin_refuses_in_its_collection_and_admits_elsewhere() {
407 let text = json!("hello");
408 let changes = [Change {
409 path: "text",
410 before: None,
411 after: Some(&text),
412 }];
413 let tree = tree();
414 match didbot_policy::evaluate(&create(None, "app.example.record", &changes), &tree) {
415 Verdict::Reject { reason, .. } => assert_eq!(reason, EXAMPLE_REASON),
416 other => panic!("expected a refusal, got {other:?}"),
417 }
418 assert!(matches!(
419 didbot_policy::evaluate(&create(None, "app.example.other", &changes), &tree),
420 Verdict::Allow
421 ));
422 }
423
424 #[test]
425 fn a_client_denial_reaches_sign_in_token_issue_and_every_write() {
426 let tree = tree();
427 let client = ClientId::parse("https://client.example/client-metadata.json").unwrap();
428 let other = ClientId::parse("https://other.example/client-metadata.json").unwrap();
429 let refused = |subject: &Subject<'_>| match didbot_policy::evaluate(subject, &tree) {
430 Verdict::Reject { reason, .. } => assert_eq!(reason, CLIENT_REASON),
431 other => panic!("expected a refusal, got {other:?}"),
432 };
433 refused(&Subject::Grant {
434 universal: universal(),
435 client_id: &client,
436 requested_scopes: &["atproto"],
437 });
438 refused(&Subject::Token {
439 account: "did:web:agent.pds.example",
440 attributes: attributes(),
441 universal: universal(),
442 client_id: &client,
443 scopes: &["atproto"],
444 });
445 refused(&create(Some(&client), "app.example.other", &[]));
446 assert!(matches!(
447 didbot_policy::evaluate(&create(Some(&other), "app.example.other", &[]), &tree),
448 Verdict::Allow
449 ));
450 }
451
452 /// The floor reaches an account no operator record mentions: a profile
453 /// write that takes the bot self-label off is refused by a tree built
454 /// from the built-ins alone.
455 #[test]
456 fn the_bot_label_may_not_be_taken_off() {
457 let tree = tree();
458 let labelled = json!({
459 "$type": "app.bsky.actor.profile",
460 "labels": {
461 "$type": "com.atproto.label.defs#selfLabels",
462 "values": [{ "val": "bot" }],
463 },
464 });
465 let bare = json!({ "$type": "app.bsky.actor.profile" });
466 let stripping = Subject::Write {
467 account: "did:web:agent.pds.example",
468 attributes: attributes(),
469 universal: universal(),
470 client_id: None,
471 collection: "app.bsky.actor.profile",
472 action: WriteAction::Update,
473 diff: Diff { changes: &[] },
474 before: Some(&labelled),
475 after: Some(&bare),
476 lookups: &[],
477 };
478 match didbot_policy::evaluate(&stripping, &tree) {
479 Verdict::Reject { reason, .. } => {
480 assert!(
481 reason.contains("the bot self-label may not be removed"),
482 "{reason}"
483 );
484 }
485 other => panic!("expected a refusal, got {other:?}"),
486 }
487 }
488}