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