Skip to main content

mesofact_core/
policy.rs

1//! Declared-vs-enforced route policy (R749-T1) — the general form of the rule
2//! [`crate::manifest`] fields keep re-learning one field at a time.
3//!
4//! **The rule.** A route policy that is declared in `mesofact.routes.ts` and
5//! not enforced by the tier actually serving it is a hard error, named at
6//! startup. Never a warning, never a skip, never fail-open.
7//!
8//! **Why a mechanism rather than another one-off check.** Express middleware is
9//! a function you can watch run; a declared policy that nothing wired looks
10//! byte-identical to one that is enforced, from outside the process and from
11//! inside the manifest alike. That class has now bitten four times — `requires:
12//! ["user"]` served fail-open by `mesofact serve` (R556-B13), `route_headers`
13//! dropped on a `worker` → `passway` front-door flip (R749-F3),
14//! `cache_policy` inert in the serve tier and `concurrency` unread by it
15//! (R746-S4's audit, W225 §2c). Each was found by someone reading the code, not
16//! by the system. Four is enough to stop fixing instances.
17//!
18//! **The shape is advertise-and-check**, reused from the bundle/runtime
19//! contract (R746-F6, W272): a tier states the policy set it implements
20//! ([`PolicySupport`]), and startup diffs that set against what the manifest
21//! actually declares ([`check_manifest`]). A tier that gains an enforcement
22//! point edits one line here; a tier that never does refuses to serve the
23//! routes it would lie about.
24//!
25//! **Unknown fields refuse too**, and that is the half that makes the class
26//! impossible rather than merely closed today. A hand-written serde slice can
27//! only miss a policy field added after it — silently, by construction, which
28//! is the exact defect. So the check walks the manifest's *raw JSON* keys and
29//! refuses any route key it cannot classify, instead of deserializing into a
30//! struct that would discard it. An old binary handed a manifest from a newer
31//! build stops, naming the field.
32//!
33//! **The escape hatch is an operator assertion, not a downgrade.** A policy may
34//! be [`PolicySupport::delegate`]d to something in front of the process — an
35//! authenticating edge, a CDN. That is the generalization of R556-B13's
36//! `--trust-edge-auth`: it stays a positive claim someone made, recorded in the
37//! process's own startup log, and it can be wrong — but it cannot be *silent*,
38//! which is the property this module is defending.
39
40use std::collections::{BTreeMap, BTreeSet};
41use std::fmt;
42
43/// A route field that changes **serving** behaviour, and can therefore fail
44/// open when the serving tier does not implement it.
45///
46/// Deliberately not "every field on [`crate::manifest::Route`]" — build-time
47/// and publish-time fields (`prerender`, `data_inputs`, `placement`,
48/// `source_reads`, `hydration`) are consumed before a request exists, so a
49/// server that ignores them cannot serve a route that quietly lacks a policy
50/// it declared. See [`STRUCTURAL_FIELDS`] for that half of the partition;
51/// every key on `Route` belongs to exactly one of the two, pinned by
52/// `every_route_field_is_classified`.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
54pub enum RoutePolicy {
55    /// `requires: ["user" | "project" | "region"]` — the auth gate. The one
56    /// whose fail-open is a confidentiality breach rather than a performance
57    /// regression, which is why R556-B13 was filed at severity high.
58    Requires,
59    /// `cache_policy: { ttl, swr, negative_ttl, vary }`.
60    CachePolicy,
61    /// `concurrency` — per-route in-flight cap.
62    Concurrency,
63    /// `resilience: { retry, timeout_ms }` (W181).
64    Resilience,
65}
66
67impl RoutePolicy {
68    /// Every policy, in declaration order. Adding a variant without adding it
69    /// here fails to compile (the exhaustive matches on `RoutePolicy` below
70    /// won't cover it).
71    pub const ALL: [RoutePolicy; 4] = [
72        RoutePolicy::Requires,
73        RoutePolicy::CachePolicy,
74        RoutePolicy::Concurrency,
75        RoutePolicy::Resilience,
76    ];
77
78    /// The manifest / `defineRoutes` field name. This is what an author reads
79    /// in their own routes file, so it is what a refusal message must name.
80    pub const fn field(self) -> &'static str {
81        match self {
82            RoutePolicy::Requires => "requires",
83            RoutePolicy::CachePolicy => "cache_policy",
84            RoutePolicy::Concurrency => "concurrency",
85            RoutePolicy::Resilience => "resilience",
86        }
87    }
88
89    /// What an author expects the field to *do*, one clause. Rendered into the
90    /// refusal so the message says what is being lost, not just which key.
91    pub const fn effect(self) -> &'static str {
92        match self {
93            RoutePolicy::Requires => "gate the route behind a resolved session",
94            RoutePolicy::CachePolicy => "cache the response for the declared ttl/swr",
95            RoutePolicy::Concurrency => "cap in-flight requests for this route",
96            RoutePolicy::Resilience => "retry and time-bound the render",
97        }
98    }
99
100    /// Parse a field name, for `--policy-delegated` / `MESOFACT_POLICY_DELEGATED`.
101    pub fn parse(field: &str) -> Option<Self> {
102        Self::ALL.into_iter().find(|p| p.field() == field)
103    }
104
105    /// For a policy carried as a JSON object, the sub-keys this build actually
106    /// implements. Empty = the policy is a scalar/array with no inner shape.
107    ///
108    /// Checked as strictly as the top level, because a policy's *sub*-field is
109    /// the same defect one level down and hides better: `serde` on
110    /// [`crate::manifest::CachePolicy`] silently drops an unknown key, so a
111    /// `cache_policy.private` added by a newer build would deserialize into a
112    /// policy that looks complete and quietly is not.
113    ///
114    /// `resilience.queue` is deliberately **absent** — W181 reserves the slot
115    /// for v2 and `defineRoutes` rejects it today, so the type exists only so
116    /// v1 binaries keep deserializing v2 manifests. Deserializing one is not
117    /// the same as honouring it; a manifest that carries a queue policy must
118    /// not be served as though the queueing happens.
119    pub const fn subfields(self) -> &'static [&'static str] {
120        match self {
121            RoutePolicy::CachePolicy => &["ttl", "swr", "negative_ttl", "vary"],
122            RoutePolicy::Resilience => &["retry", "timeout_ms"],
123            RoutePolicy::Requires | RoutePolicy::Concurrency => &[],
124        }
125    }
126
127    /// Sub-keys that exist on the manifest type and are deliberately not
128    /// implemented. Listing them is what turns "we left it out" into a
129    /// decision the completeness gate can check, and it keeps
130    /// [`subfields`](Self::subfields)'s omissions from reading as oversights.
131    pub const fn reserved_subfields(self) -> &'static [&'static str] {
132        match self {
133            // W181 § "v1 scope" — type slot only, rejected at `defineRoutes`.
134            RoutePolicy::Resilience => &["queue"],
135            _ => &[],
136        }
137    }
138}
139
140impl fmt::Display for RoutePolicy {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.write_str(self.field())
143    }
144}
145
146/// Route keys that carry no serving policy — consumed by the build, the
147/// publisher, or the router's own addressing, all of which happen before a
148/// request exists.
149///
150/// This list is the *other half* of the partition [`RoutePolicy`] opens, and
151/// it is exhaustive on purpose: a key in neither list is an unknown policy and
152/// refuses ([`Violation::UnknownField`]). Growing this list is the deliberate
153/// act of saying "this new field cannot fail open".
154pub const STRUCTURAL_FIELDS: [&str; 8] = [
155    // Routing identity — a server that ignored these would not route at all.
156    "route",
157    "mode",
158    "render_entrypoint",
159    // Build-time inference and build-time inputs.
160    "source_reads",
161    "data_inputs",
162    // Build/publish-time: which instances exist, which bundle, which script.
163    "prerender",
164    "placement",
165    "hydration",
166];
167
168/// The policy set one serving tier implements, plus the set an operator has
169/// asserted is enforced in front of it.
170///
171/// Constructed at startup by whichever binary is about to serve, so the claim
172/// lives next to the code that makes it true rather than in a doc.
173#[derive(Debug, Clone)]
174pub struct PolicySupport {
175    tier: String,
176    enforced: BTreeSet<RoutePolicy>,
177    delegated: BTreeSet<RoutePolicy>,
178}
179
180impl PolicySupport {
181    /// A tier that enforces nothing. `tier` names the binary/subcommand as an
182    /// operator would type it (`mesofact serve`), since that is the thing they
183    /// have to change.
184    pub fn new(tier: impl Into<String>) -> Self {
185        Self {
186            tier: tier.into(),
187            enforced: BTreeSet::new(),
188            delegated: BTreeSet::new(),
189        }
190    }
191
192    /// Advertise a policy this tier implements itself.
193    #[must_use]
194    pub fn enforces(mut self, policy: RoutePolicy) -> Self {
195        self.enforced.insert(policy);
196        self
197    }
198
199    /// Record an operator's assertion that something in front of this process
200    /// enforces `policy` (an authenticating edge, a CDN). Distinct from
201    /// [`enforces`](Self::enforces) so the startup log can say which of the two
202    /// is carrying a route — "we do this" and "someone says they do this" are
203    /// not the same claim and should not print the same.
204    #[must_use]
205    pub fn delegate(mut self, policy: RoutePolicy) -> Self {
206        self.delegated.insert(policy);
207        self
208    }
209
210    pub fn tier(&self) -> &str {
211        &self.tier
212    }
213
214    pub fn covers(&self, policy: RoutePolicy) -> bool {
215        self.enforced.contains(&policy) || self.delegated.contains(&policy)
216    }
217
218    pub fn is_delegated(&self, policy: RoutePolicy) -> bool {
219        self.delegated.contains(&policy)
220    }
221
222    /// Policies this tier implements, for the startup log.
223    pub fn enforced(&self) -> impl Iterator<Item = RoutePolicy> + '_ {
224        self.enforced.iter().copied()
225    }
226
227    /// Policies covered only by an operator assertion, for the startup log.
228    pub fn delegated(&self) -> impl Iterator<Item = RoutePolicy> + '_ {
229        self.delegated.iter().copied()
230    }
231}
232
233/// One route declaring something this tier will not do.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum Violation {
236    /// A known policy, declared, and neither enforced nor delegated.
237    Unenforced { route: String, policy: RoutePolicy },
238    /// A route key (or policy sub-key) nothing in this binary implements —
239    /// either a manifest from a newer build, or a slot reserved for a version
240    /// this one is not. Refusing is the whole point: a field we cannot even
241    /// name is the maximally-silent case of the defect, and it is the half that
242    /// makes the class impossible rather than merely closed today.
243    UnknownField { route: String, field: String },
244}
245
246impl Violation {
247    fn route(&self) -> &str {
248        match self {
249            Violation::Unenforced { route, .. } | Violation::UnknownField { route, .. } => route,
250        }
251    }
252
253    fn line(&self) -> String {
254        match self {
255            Violation::Unenforced { route, policy } => format!(
256                "  {route} declares `{}` — nothing here will {}",
257                policy.field(),
258                policy.effect(),
259            ),
260            Violation::UnknownField { route, field } => format!(
261                "  {route} declares `{field}`, which nothing in this binary implements — the \
262                 manifest is either newer than this build or uses a slot reserved for one"
263            ),
264        }
265    }
266}
267
268/// Every violation found, rendered as the refusal an operator reads.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct PolicyRefusal {
271    pub tier: String,
272    pub violations: Vec<Violation>,
273}
274
275impl fmt::Display for PolicyRefusal {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        let n = self.violations.len();
278        write!(
279            f,
280            "refusing to start: {n} route policy declaration(s) that `{}` does not enforce.\n",
281            self.tier,
282        )?;
283        for v in &self.violations {
284            writeln!(f, "{}", v.line())?;
285        }
286        let unknown = self
287            .violations
288            .iter()
289            .any(|v| matches!(v, Violation::UnknownField { .. }));
290        let known: BTreeSet<&'static str> = self
291            .violations
292            .iter()
293            .filter_map(|v| match v {
294                Violation::Unenforced { policy, .. } => Some(policy.field()),
295                Violation::UnknownField { .. } => None,
296            })
297            .collect();
298        write!(
299            f,
300            "A policy declared here and enforced nowhere is worse than no policy: the route \
301             serves 200 and looks correct. Either (a) drop the declaration if it was never \
302             meant to bind, (b) serve these routes on a tier that implements it, or (c) if \
303             something in front of this process really does enforce it, say so with \
304             `--policy-delegated <field>` / `MESOFACT_POLICY_DELEGATED=<field,…>`",
305        )?;
306        if !known.is_empty() {
307            write!(
308                f,
309                " (here: `{}`)",
310                known.into_iter().collect::<Vec<_>>().join(",")
311            )?;
312        }
313        f.write_str(".")?;
314        if unknown {
315            write!(
316                f,
317                " The unknown field(s) are not delegatable — upgrade this binary to one that \
318                 knows them, or rebuild the workload with a matching toolchain."
319            )?;
320        }
321        Ok(())
322    }
323}
324
325impl std::error::Error for PolicyRefusal {}
326
327/// Check a built `manifest.json` against what `support` claims to enforce.
328///
329/// Takes the **raw bytes**, not a [`crate::manifest::Manifest`], and that is
330/// load-bearing: deserializing first would discard exactly the unknown keys
331/// this is looking for. Only the route objects' key sets are inspected, so a
332/// manifest this binary cannot fully model is still checkable.
333///
334/// A manifest that does not parse as JSON is an error, not a pass — reading
335/// "no policies declared" out of a file we failed to understand is the
336/// fail-open shape this module exists to prevent.
337pub fn check_manifest(raw: &[u8], support: &PolicySupport) -> Result<(), PolicyCheckError> {
338    let doc: serde_json::Value =
339        serde_json::from_slice(raw).map_err(|e| PolicyCheckError::Unreadable(e.to_string()))?;
340    let routes = match doc.get("routes") {
341        Some(serde_json::Value::Array(routes)) => routes.as_slice(),
342        // `routes` present but not an array is a manifest shape we do not recognize.
343        Some(_) => return Err(PolicyCheckError::Unreadable("`routes` is not an array".into())),
344        None => &[],
345    };
346    let mut violations = Vec::new();
347    for route in routes {
348        let Some(obj) = route.as_object() else {
349            return Err(PolicyCheckError::Unreadable(
350                "a manifest route entry is not an object".into(),
351            ));
352        };
353        let name = obj
354            .get("route")
355            .and_then(|v| v.as_str())
356            .unwrap_or("<unnamed route>")
357            .to_string();
358        for (key, value) in obj {
359            if STRUCTURAL_FIELDS.contains(&key.as_str()) {
360                continue;
361            }
362            match RoutePolicy::parse(key) {
363                Some(policy) => {
364                    if is_declared(policy, value) && !support.covers(policy) {
365                        violations.push(Violation::Unenforced {
366                            route: name.clone(),
367                            policy,
368                        });
369                    }
370                    // Sub-keys get the same treatment, and are not delegatable
371                    // for the same reason the top-level unknowns are not: an
372                    // operator cannot assert an edge enforces a directive
373                    // neither of them can name.
374                    let known = policy.subfields();
375                    if !known.is_empty() {
376                        if let Some(obj) = value.as_object() {
377                            for sub in obj.keys() {
378                                if !known.contains(&sub.as_str()) {
379                                    violations.push(Violation::UnknownField {
380                                        route: name.clone(),
381                                        field: format!("{}.{sub}", policy.field()),
382                                    });
383                                }
384                            }
385                        }
386                    }
387                }
388                None => violations.push(Violation::UnknownField {
389                    route: name.clone(),
390                    field: key.clone(),
391                }),
392            }
393        }
394    }
395    if violations.is_empty() {
396        return Ok(());
397    }
398    // Stable order: by route, then by the message itself, so a refusal reads
399    // the same on every node and diffs cleanly in a deploy log.
400    violations.sort_by(|a, b| a.route().cmp(b.route()).then_with(|| a.line().cmp(&b.line())));
401    Err(PolicyCheckError::Refused(PolicyRefusal {
402        tier: support.tier.clone(),
403        violations,
404    }))
405}
406
407/// Why a policy check could not conclude "this is safe to serve".
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub enum PolicyCheckError {
410    /// The manifest is present and we could not read it. Not a pass.
411    Unreadable(String),
412    /// The manifest is fine and declares policy this tier will not honour.
413    Refused(PolicyRefusal),
414}
415
416impl fmt::Display for PolicyCheckError {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        match self {
419            PolicyCheckError::Unreadable(why) => write!(
420                f,
421                "refusing to start: cannot read the route manifest to check for declared \
422                 policy this binary does not enforce — {why}"
423            ),
424            PolicyCheckError::Refused(r) => r.fmt(f),
425        }
426    }
427}
428
429impl std::error::Error for PolicyCheckError {}
430
431/// Is this field's value an actual declaration, or the inert default every
432/// route carries?
433///
434/// The distinction matters because `cache_policy` is **required** in
435/// `defineRoutes` — every route in every workload has one. Treating the
436/// no-op form (`{ ttl: 0 }`, which is what an author writes to mean "never
437/// cache this") as a declaration would refuse every workload in existence and
438/// teach operators to reach straight for the escape hatch, which costs the
439/// mechanism its entire value.
440fn is_declared(policy: RoutePolicy, value: &serde_json::Value) -> bool {
441    if value.is_null() {
442        return false;
443    }
444    match policy {
445        RoutePolicy::Requires => value.as_array().is_some_and(|a| !a.is_empty()),
446        RoutePolicy::CachePolicy => {
447            let Some(obj) = value.as_object() else {
448                // A `cache_policy` that is not an object is malformed, and a
449                // malformed policy is emphatically not an absent one.
450                return true;
451            };
452            // `ttl: 0` with nothing else is "do not cache" — a statement the
453            // serving tier honours by doing nothing.
454            obj.get("ttl").and_then(|v| v.as_u64()).unwrap_or(0) > 0
455                || obj.contains_key("swr")
456                || obj.contains_key("negative_ttl")
457                || obj
458                    .get("vary")
459                    .is_some_and(|v| v.as_array().is_some_and(|a| !a.is_empty()))
460        }
461        RoutePolicy::Concurrency => true,
462        RoutePolicy::Resilience => value
463            .as_object()
464            .is_some_and(|o| o.values().any(|v| !v.is_null())),
465    }
466}
467
468/// Group a refusal's violations by policy, for a caller that wants to log the
469/// summary rather than the whole list.
470pub fn by_policy(refusal: &PolicyRefusal) -> BTreeMap<&'static str, Vec<&str>> {
471    let mut out: BTreeMap<&'static str, Vec<&str>> = BTreeMap::new();
472    for v in &refusal.violations {
473        let key = match v {
474            Violation::Unenforced { policy, .. } => policy.field(),
475            Violation::UnknownField { .. } => "<unknown>",
476        };
477        out.entry(key).or_default().push(v.route());
478    }
479    out
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use crate::manifest::{
486        CachePolicy, Hydration, Prerender, Requires, ResiliencePolicy, ResolvedPlacement,
487        RetryPolicy, Route, RouteMode,
488    };
489
490    fn serve_tier() -> PolicySupport {
491        PolicySupport::new("mesofact serve").enforces(RoutePolicy::Resilience)
492    }
493
494    fn manifest(routes: &str) -> Vec<u8> {
495        format!(r#"{{"version":"1","build_id":"b","routes":[{routes}]}}"#).into_bytes()
496    }
497
498    const PLAIN: &str = r#"{"route":"/","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":0}}"#;
499
500    /// THE test this ticket exists for, written as the absence of a success
501    /// path: a declared-and-unenforced policy must not produce `Ok`. Phrased
502    /// this way — rather than asserting on the error string — because the
503    /// failure mode is invisible by construction, so the thing worth pinning
504    /// is that no input reaches the serving side at all.
505    #[test]
506    fn a_declared_unenforced_policy_has_no_success_path() {
507        let tier = serve_tier();
508        let declarations = [
509            r#""requires":["user"]"#,
510            r#""cache_policy":{"ttl":3600}"#,
511            r#""cache_policy":{"ttl":0,"swr":60}"#,
512            r#""cache_policy":{"ttl":0,"vary":["accept-language"]}"#,
513            r#""concurrency":4"#,
514            r#""future_policy":{"limit":2}"#,
515        ];
516        for decl in declarations {
517            let raw = manifest(&format!(
518                r#"{{"route":"/p","mode":"ssr","render_entrypoint":"e.js","cache_policy":{{"ttl":0}},{decl}}}"#
519            ));
520            match check_manifest(&raw, &tier) {
521                Err(PolicyCheckError::Refused(_)) => {}
522                other => panic!(
523                    "declaring {decl} on a tier that does not enforce it returned {other:?}; \
524                     that is the silent no-op R749-T1 forbids ({} policies known)",
525                    RoutePolicy::ALL.len(),
526                ),
527            }
528        }
529    }
530
531    #[test]
532    fn the_refusal_names_the_route_and_the_field() {
533        let raw = manifest(
534            r#"{"route":"/private","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"requires":["user"]}"#,
535        );
536        let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
537        assert!(err.contains("/private"), "{err}");
538        assert!(err.contains("requires"), "{err}");
539        assert!(err.contains("--policy-delegated"), "{err}");
540    }
541
542    #[test]
543    fn the_inert_cache_policy_every_route_carries_is_not_a_declaration() {
544        assert!(check_manifest(&manifest(PLAIN), &serve_tier()).is_ok());
545    }
546
547    #[test]
548    fn an_enforced_policy_passes_and_a_delegated_one_does_too() {
549        let raw = manifest(
550            r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":5000},"requires":["user"]}"#,
551        );
552        assert!(check_manifest(&raw, &serve_tier()).is_err());
553        let trusting = serve_tier().delegate(RoutePolicy::Requires);
554        assert!(check_manifest(&raw, &trusting).is_ok());
555        assert!(trusting.is_delegated(RoutePolicy::Requires));
556        assert!(!trusting.is_delegated(RoutePolicy::Resilience));
557    }
558
559    /// An empty `resilience: {}` block survives a round-trip through
560    /// `skip_serializing_if` as `{}`; nothing is being asked for, so nothing
561    /// is unenforced.
562    #[test]
563    fn an_empty_policy_block_is_not_a_declaration() {
564        let raw = manifest(
565            r#"{"route":"/a","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{},"requires":[]}"#,
566        );
567        assert!(check_manifest(&raw, &PolicySupport::new("bare")).is_ok());
568    }
569
570    #[test]
571    fn an_unparseable_manifest_is_not_a_pass() {
572        assert!(matches!(
573            check_manifest(b"{ not json", &serve_tier()),
574            Err(PolicyCheckError::Unreadable(_))
575        ));
576        assert!(matches!(
577            check_manifest(br#"{"routes":"nope"}"#, &serve_tier()),
578            Err(PolicyCheckError::Unreadable(_))
579        ));
580    }
581
582    #[test]
583    fn an_unknown_field_is_refused_and_not_delegatable() {
584        let raw = manifest(
585            r#"{"route":"/x","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"rate_limit":{"rps":10}}"#,
586        );
587        let mut permissive = serve_tier();
588        for p in RoutePolicy::ALL {
589            permissive = permissive.delegate(p);
590        }
591        let err = check_manifest(&raw, &permissive).unwrap_err().to_string();
592        assert!(err.contains("rate_limit"), "{err}");
593        assert!(err.contains("not delegatable"), "{err}");
594    }
595
596    /// The completeness gate. Every serde key on [`Route`] must be classified
597    /// as either a [`RoutePolicy`] or a [`STRUCTURAL_FIELDS`] entry — so adding
598    /// a field to the manifest without deciding "can this fail open?" fails
599    /// here rather than shipping as the next R556-B13.
600    ///
601    /// Built from a maximally-populated `Route` rather than a hand-listed set,
602    /// because a hand-listed set is exactly the artifact that goes stale.
603    #[test]
604    fn every_route_field_is_classified() {
605        let full = Route {
606            route: "/x/:id".into(),
607            mode: RouteMode::Ssr,
608            render_entrypoint: "dist/server/x.js".into(),
609            requires: Some(vec![Requires::User]),
610            source_reads: Some(vec!["s".into()]),
611            data_inputs: Some(vec!["d.json".into()]),
612            cache_policy: CachePolicy {
613                ttl: 1,
614                swr: Some(1),
615                negative_ttl: Some(1),
616                vary: Some(vec!["accept".into()]),
617            },
618            concurrency: Some(1),
619            hydration: Some(Hydration {
620                script: "s.js".into(),
621                code_split: vec![],
622            }),
623            prerender: Some(Prerender::Deferred { deferred: true }),
624            placement: Some(ResolvedPlacement::Host),
625            resilience: Some(ResiliencePolicy {
626                retry: Some(RetryPolicy {
627                    attempts: 2,
628                    backoff_ms: vec![10],
629                    retry_on: None,
630                    budget_ms: None,
631                }),
632                queue: None,
633                timeout_ms: Some(1),
634            }),
635        };
636        let json = serde_json::to_value(&full).unwrap();
637        let unclassified: Vec<&String> = json
638            .as_object()
639            .unwrap()
640            .keys()
641            .filter(|k| {
642                !STRUCTURAL_FIELDS.contains(&k.as_str()) && RoutePolicy::parse(k).is_none()
643            })
644            .collect();
645        assert!(
646            unclassified.is_empty(),
647            "manifest Route gained field(s) {unclassified:?} that are neither a RoutePolicy nor \
648             STRUCTURAL_FIELDS. Decide which: if a serving tier ignoring it would silently drop \
649             behaviour an author asked for, it is a RoutePolicy and every tier must advertise \
650             it; otherwise add it to STRUCTURAL_FIELDS with a reason.",
651        );
652        // And the reverse: a policy nothing on `Route` carries would refuse a
653        // manifest nobody can produce.
654        for policy in RoutePolicy::ALL {
655            let value = json
656                .get(policy.field())
657                .unwrap_or_else(|| panic!(
658                    "RoutePolicy::{policy:?} names `{}`, which is not a field on manifest::Route",
659                    policy.field(),
660                ));
661            // Same gate one level down: every sub-key must be implemented or
662            // explicitly reserved, so a new `cache_policy` directive cannot
663            // land as a field serde quietly drops.
664            let Some(obj) = value.as_object() else { continue };
665            let unclassified: Vec<&String> = obj
666                .keys()
667                .filter(|k| {
668                    !policy.subfields().contains(&k.as_str())
669                        && !policy.reserved_subfields().contains(&k.as_str())
670                })
671                .collect();
672            assert!(
673                unclassified.is_empty(),
674                "`{}` gained sub-field(s) {unclassified:?}: add them to RoutePolicy::subfields \
675                 once a tier implements them, or to reserved_subfields with the doc that \
676                 reserves the slot",
677                policy.field(),
678            );
679        }
680    }
681
682    /// `resilience.queue` is a v2 slot `defineRoutes` rejects — so a manifest
683    /// carrying one did not come from `defineRoutes`, and deserializing it is
684    /// not the same as queueing anything.
685    #[test]
686    fn a_reserved_subfield_refuses_even_where_its_parent_policy_is_enforced() {
687        let raw = manifest(
688            r#"{"route":"/q","mode":"ssr","render_entrypoint":"e.js","cache_policy":{"ttl":0},"resilience":{"timeout_ms":100,"queue":{"queue":"q","ack":"on_enqueue"}}}"#,
689        );
690        let err = check_manifest(&raw, &serve_tier()).unwrap_err().to_string();
691        assert!(err.contains("resilience.queue"), "{err}");
692    }
693
694    #[test]
695    fn an_unknown_cache_directive_refuses_instead_of_being_dropped_by_serde() {
696        let raw = manifest(
697            r#"{"route":"/c","mode":"static","render_entrypoint":"e.js","cache_policy":{"ttl":60,"shared_max_age":30}}"#,
698        );
699        let tier = serve_tier().enforces(RoutePolicy::CachePolicy);
700        let err = check_manifest(&raw, &tier).unwrap_err().to_string();
701        assert!(err.contains("cache_policy.shared_max_age"), "{err}");
702    }
703}