Skip to main content

clawft_kernel/
overlay_runtime.rs

1//! A project kernel's live governance: boot, reload and parent updates
2//! (ADR-103 A6, D8, Phase 2 package E).
3//!
4//! * [`prepare`] runs early in boot, before any subsystem is built: it reads
5//!   the project certificate, the signed parent policy and the overlay, checks
6//!   them and merges. Any failure is a boot refusal (fail closed, decision 2);
7//!   a project never starts with fewer rules than its parent.
8//! * [`Prepared::apply_limits`] enforces the merged `max_processes` (the
9//!   process table's cap) and `spawn_budget` (the concurrent sub-agent spawn
10//!   cap, `kernel.agent.subagents.max_per_conv`) by lowering the kernel config
11//!   before it is used. Both are boot-time caps: a pushed policy that lowers
12//!   them reports `restart_required` instead of pretending to apply live.
13//! * [`OverlayRuntime`] owns the running state. [`OverlayRuntime::reload`] is
14//!   the only thing that re-reads the files; an overlay edited on disk does
15//!   nothing until it is called. [`OverlayRuntime::apply_parent_update`]
16//!   accepts a pushed policy, which must carry a valid user signature and a
17//!   version no older than the newest accepted. Every accepted change appends
18//!   `governance.overlay.applied` and publishes the new effective hash to the
19//!   chain's `rule_hash` provider; every refused one appends
20//!   `governance.overlay.rejected` and leaves the running rules alone.
21//!
22//! The chain stamps `rule_hash` through a lock-free [`RuleHashCell`]; the gate
23//! swap and the cell write happen under the gate's write lock, so a decision
24//! is never chained with the other generation's hash. Lock order: the gate
25//! lock is taken before the chain lock (a check appends under it); nothing
26//! takes the gate lock while holding the chain lock.
27
28use std::path::PathBuf;
29use std::sync::{Arc, Mutex, RwLock};
30
31use clawft_types::config::KernelConfig;
32use clawft_types::project::canon::hex_encode;
33use clawft_types::runtime_paths::RuntimePaths;
34use serde_json::json;
35
36use crate::chain::ChainManager;
37use crate::chain_rule_hash::RuleHashCell;
38use crate::gate::{GateBackend, GateDecision, GovernanceGate, GovernanceSnapshot};
39use crate::governance_overlay::{Effective, Overlay, OverlayError, load_overlay, merge};
40pub use crate::overlay_trust::{REVOKED_FILE, USER_PIN_FILE, VERSION_PIN_FILE, write_user_pin};
41use crate::overlay_trust::{chain_history, check_version, load_user_pubkey, read_pin, write_pin};
42use crate::parent_policy::{
43    ParentPolicy, ParentPolicyError, load_parent_policy, verify_parent_policy,
44};
45
46/// Fallback engine threshold when neither parent nor overlay set one.
47pub const DEFAULT_RISK_THRESHOLD: f64 = 0.7;
48#[cfg(test)]
49thread_local! {
50    /// Tests boot with `RuntimePaths::at`; this lets one name a child root.
51    pub(crate) static TEST_CHILD_PATHS: std::cell::RefCell<Option<RuntimePaths>> =
52        const { std::cell::RefCell::new(None) };
53}
54
55/// The child's paths: `paths` itself when it is a child root. A project
56/// profile on any other root is refused (package H makes
57/// `RuntimePaths::resolve()` return the child root for a spawned child).
58pub fn child_paths(paths: &RuntimePaths) -> Result<RuntimePaths, OverlayError> {
59    #[cfg(test)]
60    if let Some(p) = TEST_CHILD_PATHS.with(|c| c.borrow().clone()) {
61        return Ok(p);
62    }
63    #[cfg(feature = "test-support")]
64    if let Some(p) = test_support::child_paths() {
65        return Ok(p);
66    }
67    paths.child_id().ok_or(OverlayError::NotAChild)?;
68    Ok(paths.clone())
69}
70
71/// Process-wide child-root override for other crates' integration tests,
72/// which boot a project-profile kernel before package H's supervisor exists.
73/// Only compiled with the `test-support` feature, which a release build
74/// refuses (see the `compile_error!` in `lib.rs`).
75#[cfg(feature = "test-support")]
76pub mod test_support {
77    use super::RuntimePaths;
78    use std::sync::Mutex;
79
80    static CHILD_PATHS: Mutex<Option<RuntimePaths>> = Mutex::new(None);
81
82    /// Make [`super::child_paths`] return `paths` for the rest of the process.
83    pub fn set_child_paths(paths: Option<RuntimePaths>) {
84        *CHILD_PATHS.lock().unwrap_or_else(|e| e.into_inner()) = paths;
85    }
86
87    pub(super) fn child_paths() -> Option<RuntimePaths> {
88        CHILD_PATHS
89            .lock()
90            .unwrap_or_else(|e| e.into_inner())
91            .clone()
92    }
93
94    /// A project the child-boot tests run as.
95    pub const FIXTURE_PROJECT_ID: &str = "01JB8Z3Q0V6X9KQ4M2N7T5R1WD";
96
97    /// Build a REAL child root under `base` and make it this process's child
98    /// root. Nothing is bypassed: the overlay's own checks run over it.
99    ///
100    /// Written exactly as the user daemon and supervisor will write them: a
101    /// user key (seed `[7; 32]`) that signs the project certificate
102    /// (`ProjectCert::sign`) and an empty-rules parent policy
103    /// (`parent_policy::export_rules_to`), the `user.pub` pin
104    /// (`write_user_pin`), and a 0600 `project.key` matching the certificate.
105    /// The version pin is written by `Prepared::commit`, as on a real boot.
106    pub fn install_child_fixture(base: &std::path::Path) -> RuntimePaths {
107        use chrono::{Duration, Utc};
108        use clawft_types::config::overlay::Limits;
109        use clawft_types::project::cert::{CertRequest, ProjectCert};
110        use ed25519_dalek::SigningKey;
111
112        let run = base.join("run").join(FIXTURE_PROJECT_ID);
113        let root = base.join("project");
114        std::fs::create_dir_all(&run).expect("run dir");
115        std::fs::create_dir_all(root.join(".weftos")).expect("project .weftos");
116        let paths = RuntimePaths::child_at(&run, FIXTURE_PROJECT_ID, &root).expect("child paths");
117
118        let user_key = SigningKey::from_bytes(&[7u8; 32]);
119        let project_key = SigningKey::from_bytes(&[3u8; 32]);
120        let cert = ProjectCert::sign(
121            &user_key,
122            &CertRequest {
123                project_id: FIXTURE_PROJECT_ID.into(),
124                project_pubkey: project_key.verifying_key().to_bytes(),
125                serial: 1,
126                issued_at: Utc::now() - Duration::minutes(1),
127                expires_at: None,
128            },
129        );
130        let cert_path = paths.project_cert().expect("cert path");
131        std::fs::write(&cert_path, serde_json::to_vec(&cert).expect("cert json")).expect("cert");
132        crate::parent_policy::write_atomic_0600(
133            &paths.project_key().expect("key path"),
134            &project_key.to_bytes(),
135        )
136        .expect("project.key");
137        crate::parent_policy::export_rules_to(
138            &paths.parent_policy(),
139            Vec::new(),
140            0.8,
141            false,
142            &Limits::default(),
143            &user_key,
144        )
145        .expect("signed parent policy");
146        crate::overlay_trust::write_user_pin(&run, &user_key.verifying_key().to_bytes())
147            .expect("user.pub pin");
148        set_child_paths(Some(paths.clone()));
149        paths
150    }
151}
152
153/// A gate whose inner [`GovernanceGate`] is replaced atomically on update.
154pub struct OverlayGate {
155    inner: RwLock<GovernanceGate>,
156}
157
158impl OverlayGate {
159    fn new(gate: GovernanceGate) -> Self {
160        Self {
161            inner: RwLock::new(gate),
162        }
163    }
164
165    /// Replace the rules and publish `hash` in one step (see module docs).
166    fn swap(&self, mut gate: GovernanceGate, hash: [u8; 32], cell: &RuleHashCell) {
167        let mut w = self.inner.write().unwrap_or_else(|e| e.into_inner());
168        // Keep what the kernel configured on the running gate (exemptions,
169        // rate limit, scorer); a fresh gate would fall back to defaults.
170        gate.inherit_config(&w);
171        *w = gate;
172        cell.set(Some(hash));
173    }
174}
175
176impl GateBackend for OverlayGate {
177    fn check(&self, agent_id: &str, action: &str, context: &serde_json::Value) -> GateDecision {
178        // The read lock spans the whole evaluation: a concurrent swap waits.
179        self.inner
180            .read()
181            .unwrap_or_else(|e| e.into_inner())
182            .check(agent_id, action, context)
183    }
184
185    fn governance_snapshot(&self) -> Option<GovernanceSnapshot> {
186        self.inner
187            .read()
188            .unwrap_or_else(|e| e.into_inner())
189            .governance_snapshot()
190    }
191}
192
193/// The hashes and version of the rules a kernel is running.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Applied {
196    /// Hex.
197    pub parent_hash: String,
198    /// Hex.
199    pub overlay_hash: String,
200    /// Hex; the chain's `rule_hash`.
201    pub effective_hash: String,
202    /// Parent policy version.
203    pub parent_version: u64,
204    /// Rules in the engine.
205    pub rule_count: usize,
206    /// `max_processes` / `spawn_budget` changed since boot and only a restart
207    /// applies them.
208    pub restart_required: bool,
209}
210
211impl Applied {
212    fn of(e: &Effective, boot: &Effective) -> Self {
213        let restart_required = e.limits.max_processes != boot.limits.max_processes
214            || e.limits.spawn_budget != boot.limits.spawn_budget;
215        Self {
216            parent_hash: hex_encode(&e.parent_hash),
217            overlay_hash: hex_encode(&e.overlay_hash),
218            effective_hash: hex_encode(&e.effective_hash),
219            parent_version: e.parent_version,
220            rule_count: e.rules.len(),
221            restart_required,
222        }
223    }
224
225    /// JSON for RPC replies and chain payloads.
226    pub fn to_json(&self) -> serde_json::Value {
227        json!({
228            "parent_hash": self.parent_hash,
229            "overlay_hash": self.overlay_hash,
230            "effective_hash": self.effective_hash,
231            "parent_version": self.parent_version,
232            "rule_count": self.rule_count,
233            "restart_required": self.restart_required,
234        })
235    }
236}
237
238/// Everything boot learned about a project's governance, before the chain
239/// exists.
240pub struct Prepared {
241    nested: bool,
242    paths: RuntimePaths,
243    user_pubkey: [u8; 32],
244    user_pin: bool,
245    parent: ParentPolicy,
246    overlay: Overlay,
247    overlay_present: bool,
248    pinned: Option<u64>,
249    effective: Effective,
250    cell: Arc<RuleHashCell>,
251}
252
253/// Read, verify and merge the certificate, parent policy and overlay of the
254/// child at `paths`. Fails closed.
255pub fn prepare(paths: &RuntimePaths) -> Result<Prepared, OverlayError> {
256    let paths = child_paths(paths)?;
257    let (user_pubkey, user_pin) = load_user_pubkey(&paths)?;
258    let parent = load_parent_policy(&paths.parent_policy())?;
259    verify_parent_policy(&parent, &user_pubkey)?;
260    let pinned = read_pin(&paths)?;
261    check_version(&parent, pinned)?;
262    let overlay_present = paths.overlay().is_some_and(|p| p.exists());
263    let overlay = match paths.overlay() {
264        Some(p) => load_overlay(&p)?,
265        None => Overlay::empty(),
266    };
267    let effective = merge(&parent, &overlay)?;
268    let cell = Arc::new(RuleHashCell::new());
269    cell.set(Some(effective.effective_hash));
270    Ok(Prepared {
271        nested: false,
272        paths,
273        user_pubkey,
274        user_pin,
275        parent,
276        overlay,
277        overlay_present,
278        pinned,
279        effective,
280        cell,
281    })
282}
283
284impl Prepared {
285    /// Boot-time history checks against the project chain, then persist the
286    /// rollback pin. Call once the chain is restored and before anything is
287    /// appended to it.
288    ///
289    /// * the pin is missing but the chain records an applied overlay: someone
290    ///   removed it to roll the parent policy back, so boot is refused;
291    /// * `overlay.toml` is missing but the last applied overlay was not empty:
292    ///   deleting the file must not clear the overlay (an empty file does).
293    pub fn commit(&self, chain: &ChainManager) -> Result<(), OverlayError> {
294        let h = chain_history(chain);
295        if self.pinned.is_none() && h.any_applied {
296            return Err(OverlayError::PinMissing);
297        }
298        // The pin file can be lowered or zeroed; the chain remembers the
299        // highest version ever applied, so neither the file's version nor the
300        // pin may be below it.
301        if let Some(max) = h.max_parent_version {
302            for have in [Some(self.parent.version), self.pinned]
303                .into_iter()
304                .flatten()
305            {
306                if have < max {
307                    return Err(ParentPolicyError::Rollback { have, pinned: max }.into());
308                }
309            }
310        }
311        // A pin that was in use must not silently disappear.
312        if h.user_pin_used && !self.user_pin {
313            return Err(OverlayError::Cert(
314                "a user.pub pin was in use on an earlier boot but is now absent".into(),
315            ));
316        }
317        let empty = hex_encode(&Overlay::empty().hash);
318        if let Some(last) = &h.last_overlay_hash
319            && !self.overlay_present
320            && *last != empty
321        {
322            return Err(OverlayError::OverlayMissing);
323        }
324        if self.pinned.is_none_or(|p| self.parent.version > p) {
325            write_pin(&self.paths, self.parent.version)?;
326        }
327        Ok(())
328    }
329
330    /// Lower `kc` to the merged limits. A limit can only go down.
331    pub fn apply_limits(&self, kc: &mut KernelConfig) {
332        let l = &self.effective.limits;
333        if let Some(n) = l.max_processes {
334            kc.max_processes = kc.max_processes.min(u32::try_from(n).unwrap_or(u32::MAX));
335        }
336        if let Some(b) = l.spawn_budget {
337            let sub = &mut kc.agent.get_or_insert_with(Default::default).subagents;
338            sub.max_per_conv = sub.max_per_conv.min(u32::try_from(b).unwrap_or(u32::MAX));
339            if b == 0 {
340                sub.enabled = false;
341            }
342        }
343    }
344
345    /// The merged rules (they replace the default set).
346    pub fn rules(&self) -> &[crate::governance::GovernanceRule] {
347        &self.effective.rules
348    }
349
350    /// Engine threshold and human-approval flag for the gate.
351    pub fn engine_params(&self) -> (f64, bool) {
352        (
353            self.effective.risk_threshold(DEFAULT_RISK_THRESHOLD),
354            self.effective.human_approval(false),
355        )
356    }
357
358    /// Hashes of what boot merged.
359    pub fn applied(&self) -> Applied {
360        Applied::of(&self.effective, &self.effective)
361    }
362
363    /// Start stamping `rule_hash` on every chain append. Call before the
364    /// first governance event so genesis carries it.
365    pub fn install_provider(&self, chain: &ChainManager) {
366        chain.set_rule_hash_provider(self.cell.provider());
367    }
368
369    /// Wrap the boot gate (already built from [`Self::rules`] and
370    /// [`Self::engine_params`]) so it can be replaced, and record the
371    /// applied rules on the chain.
372    pub fn into_runtime(
373        self,
374        gate: GovernanceGate,
375        chain: Arc<ChainManager>,
376    ) -> (Arc<dyn GateBackend>, Arc<OverlayRuntime>) {
377        let gate = Arc::new(OverlayGate::new(gate));
378        let applied = self.applied();
379        chain.append(
380            "governance",
381            "governance.overlay.applied",
382            Some(applied_payload(
383                &applied,
384                "boot",
385                self.user_pin,
386                &self.user_pubkey,
387            )),
388        );
389        let rt = Arc::new(OverlayRuntime {
390            gate: Arc::clone(&gate),
391            cell: self.cell,
392            chain,
393            nested: self.nested,
394            paths: self.paths,
395            user_pubkey: self.user_pubkey,
396            user_pin: self.user_pin,
397            boot: self.effective.clone(),
398            state: Mutex::new(State {
399                parent: self.parent,
400                overlay: self.overlay,
401                effective: self.effective,
402            }),
403        });
404        (gate as Arc<dyn GateBackend>, rt)
405    }
406}
407
408/// Chain payload of an applied change: the hashes plus the trust root in use
409/// (`user_pin` says whether a `user.pub` pin backed it).
410fn applied_payload(
411    a: &Applied,
412    source: &str,
413    user_pin: bool,
414    user_pubkey: &[u8; 32],
415) -> serde_json::Value {
416    let mut v = a.to_json();
417    v["source"] = json!(source);
418    v["user_pin"] = json!(user_pin);
419    v["user_key_id"] = json!(clawft_types::project::cert::key_id(user_pubkey));
420    v
421}
422
423struct State {
424    parent: ParentPolicy,
425    overlay: Overlay,
426    effective: Effective,
427}
428
429/// The running project governance.
430pub struct OverlayRuntime {
431    nested: bool,
432    gate: Arc<OverlayGate>,
433    cell: Arc<RuleHashCell>,
434    chain: Arc<ChainManager>,
435    paths: RuntimePaths,
436    user_pubkey: [u8; 32],
437    user_pin: bool,
438    boot: Effective,
439    state: Mutex<State>,
440}
441
442impl OverlayRuntime {
443    /// The rules this kernel is running.
444    pub fn applied(&self) -> Applied {
445        let s = self.state.lock().unwrap_or_else(|e| e.into_inner());
446        Applied::of(&s.effective, &self.boot)
447    }
448
449    /// The `rule_hash` the chain is stamping now.
450    pub fn rule_hash(&self) -> Option<[u8; 32]> {
451        self.cell.get()
452    }
453
454    /// Re-check that the trust root is unchanged: certificate, expiry,
455    /// revocation and the user-key pin (see [`load_user_pubkey`]).
456    fn recheck_trust(&self) -> Result<(), OverlayError> {
457        let (pk, pinned) = if self.nested {
458            nested_pin(&self.paths).map(|pk| (pk, true))?
459        } else {
460            load_user_pubkey(&self.paths)?
461        };
462        if self.user_pin && !pinned {
463            return Err(OverlayError::Cert(
464                "the user.pub pin that was in use is gone".into(),
465            ));
466        }
467        if pk != self.user_pubkey {
468            return Err(OverlayError::Cert(
469                "the trusted user key changed since boot; restart the kernel".into(),
470            ));
471        }
472        Ok(())
473    }
474
475    /// Re-read the overlay (and a newer parent policy, if the file holds one)
476    /// and apply. The only path by which a disk edit takes effect. A deleted
477    /// overlay file is refused once a non-empty overlay is in force; an empty
478    /// file is the deliberate way to clear it.
479    pub fn reload(&self) -> Result<Applied, OverlayError> {
480        let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
481        let result = (|| {
482            self.recheck_trust()?;
483            let overlay = match self.paths.overlay() {
484                Some(p) if p.exists() => load_overlay(&p)?,
485                _ if s.overlay != Overlay::empty() => return Err(OverlayError::OverlayMissing),
486                _ => Overlay::empty(),
487            };
488            let mut parent = s.parent.clone();
489            let disk = load_parent_policy(&self.paths.parent_policy())?;
490            verify_parent_policy(&disk, &self.user_pubkey)?;
491            if self.nested && disk.sig != s.parent.sig {
492                return Err(OverlayError::Cert(
493                    "nested master policy changes require a new signed boot contract and restart"
494                        .into(),
495                ));
496            }
497            // An older (validly signed) file on disk is ignored, not applied.
498            if disk.version >= parent.version {
499                parent = disk;
500            }
501            self.apply_locked(&mut s, parent, overlay)
502        })();
503        self.record(&result, "reload");
504        result
505    }
506
507    /// Accept a pushed policy: valid user signature, version no older than
508    /// the newest accepted, merged with the overlay as last loaded (the file
509    /// on disk is not consulted). The project must still be unrevoked and its
510    /// certificate unexpired.
511    pub fn apply_parent_update(&self, policy: ParentPolicy) -> Result<Applied, OverlayError> {
512        let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
513        let result = (|| {
514            self.recheck_trust()?;
515            verify_parent_policy(&policy, &self.user_pubkey)?;
516            if self.nested && policy.sig != s.parent.sig {
517                return Err(OverlayError::Cert(
518                    "nested master policy changes require a new signed boot contract and restart"
519                        .into(),
520                ));
521            }
522            let overlay = s.overlay.clone();
523            self.apply_locked(&mut s, policy, overlay)
524        })();
525        self.record(&result, "parent.update");
526        result
527    }
528
529    /// The effective rules and engine parameters now in force.
530    pub fn effective_rules(&self) -> (Vec<crate::governance::GovernanceRule>, f64, bool) {
531        let s = self.state.lock().unwrap_or_else(|e| e.into_inner());
532        (
533            s.effective.rules.clone(),
534            s.effective.risk_threshold(DEFAULT_RISK_THRESHOLD),
535            s.effective.human_approval(false),
536        )
537    }
538
539    /// [`Self::effective_rules`] and the effective hash (hex) from ONE lock
540    /// acquisition, so the pair always describes the same generation.
541    pub fn effective_rules_and_hash(
542        &self,
543    ) -> ((Vec<crate::governance::GovernanceRule>, f64, bool), String) {
544        let s = self.state.lock().unwrap_or_else(|e| e.into_inner());
545        (
546            (
547                s.effective.rules.clone(),
548                s.effective.risk_threshold(DEFAULT_RISK_THRESHOLD),
549                s.effective.human_approval(false),
550            ),
551            Applied::of(&s.effective, &self.boot).effective_hash,
552        )
553    }
554
555    fn apply_locked(
556        &self,
557        s: &mut State,
558        parent: ParentPolicy,
559        overlay: Overlay,
560    ) -> Result<Applied, OverlayError> {
561        check_version(&parent, Some(s.parent.version))?;
562        let effective = merge(&parent, &overlay)?;
563        let (threshold, human) = (
564            effective.risk_threshold(DEFAULT_RISK_THRESHOLD),
565            effective.human_approval(false),
566        );
567        let mut gate = GovernanceGate::new(threshold, human).with_chain(Arc::clone(&self.chain));
568        for r in effective.rules.iter().cloned() {
569            gate = gate.add_rule(r);
570        }
571        if parent.version > s.parent.version {
572            write_pin(&self.paths, parent.version)?;
573        }
574        self.gate.swap(gate, effective.effective_hash, &self.cell);
575        let applied = Applied::of(&effective, &self.boot);
576        s.parent = parent;
577        s.overlay = overlay;
578        s.effective = effective;
579        Ok(applied)
580    }
581
582    fn record(&self, result: &Result<Applied, OverlayError>, source: &str) {
583        match result {
584            Ok(a) => {
585                self.chain.append(
586                    "governance",
587                    "governance.overlay.applied",
588                    Some(applied_payload(a, source, self.user_pin, &self.user_pubkey)),
589                );
590            }
591            Err(e) => {
592                self.chain.append(
593                    "governance",
594                    "governance.overlay.rejected",
595                    Some(json!({"source": source, "key": e.key(), "reason": e.to_string()})),
596                );
597            }
598        }
599    }
600
601    /// Path of the overlay file this kernel reads on reload.
602    pub fn overlay_path(&self) -> Option<PathBuf> {
603        self.paths.overlay()
604    }
605}
606
607// Only the verified nested-user boot path installs this contract. A config
608// string cannot select this trust path or turn off certificate checks for projects.
609static NESTED_POLICY: std::sync::OnceLock<(RuntimePaths, [u8; 32], ParentPolicy)> =
610    std::sync::OnceLock::new();
611
612pub fn install_nested_policy(
613    run: PathBuf,
614    home: PathBuf,
615    id: &str,
616    master: [u8; 32],
617    policy: ParentPolicy,
618) -> Result<(), String> {
619    verify_parent_policy(&policy, &master).map_err(|e| e.to_string())?;
620    let paths = RuntimePaths::child_at(&run, id, &home)
621        .ok_or_else(|| "invalid nested instance id".to_owned())?;
622    NESTED_POLICY
623        .set((paths, master, policy))
624        .map_err(|_| "nested policy already installed".into())
625}
626
627fn nested_pin(paths: &RuntimePaths) -> Result<[u8; 32], OverlayError> {
628    if paths.revoked_marker().exists() {
629        return Err(OverlayError::Cert("nested instance revoked".into()));
630    }
631    let text = std::fs::read_to_string(paths.root().join(USER_PIN_FILE))
632        .map_err(|e| OverlayError::Cert(e.to_string()))?;
633    clawft_types::project::canon::hex_decode::<32>(text.trim())
634        .ok_or_else(|| OverlayError::Cert("missing or malformed nested master pin".into()))
635}
636
637/// The existing overlay runtime handles nested users too: signature verification,
638/// version history, limits, gate swap and every event's rule hash are unchanged.
639pub fn prepare_nested() -> Result<Option<Prepared>, OverlayError> {
640    let Some((paths, master, parent)) = NESTED_POLICY.get() else {
641        return Ok(None);
642    };
643    if nested_pin(paths)? != *master {
644        return Err(OverlayError::Cert("nested master pin changed".into()));
645    }
646    verify_parent_policy(parent, master)?;
647    let pinned = read_pin(paths)?;
648    check_version(parent, pinned)?;
649    let overlay_present = paths.overlay().is_some_and(|p| p.exists());
650    let overlay = match paths.overlay() {
651        Some(p) => load_overlay(&p)?,
652        None => Overlay::empty(),
653    };
654    let effective = merge(parent, &overlay)?;
655    let cell = Arc::new(RuleHashCell::new());
656    cell.set(Some(effective.effective_hash));
657    Ok(Some(Prepared {
658        nested: true,
659        paths: paths.clone(),
660        user_pubkey: *master,
661        user_pin: true,
662        parent: parent.clone(),
663        overlay,
664        overlay_present,
665        pinned,
666        effective,
667        cell,
668    }))
669}