1use 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
46pub const DEFAULT_RISK_THRESHOLD: f64 = 0.7;
48#[cfg(test)]
49thread_local! {
50 pub(crate) static TEST_CHILD_PATHS: std::cell::RefCell<Option<RuntimePaths>> =
52 const { std::cell::RefCell::new(None) };
53}
54
55pub 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#[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 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 pub const FIXTURE_PROJECT_ID: &str = "01JB8Z3Q0V6X9KQ4M2N7T5R1WD";
96
97 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
153pub 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 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct Applied {
196 pub parent_hash: String,
198 pub overlay_hash: String,
200 pub effective_hash: String,
202 pub parent_version: u64,
204 pub rule_count: usize,
206 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 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
238pub 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
253pub 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 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 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 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 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 pub fn rules(&self) -> &[crate::governance::GovernanceRule] {
347 &self.effective.rules
348 }
349
350 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 pub fn applied(&self) -> Applied {
360 Applied::of(&self.effective, &self.effective)
361 }
362
363 pub fn install_provider(&self, chain: &ChainManager) {
366 chain.set_rule_hash_provider(self.cell.provider());
367 }
368
369 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
408fn 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
429pub 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 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 pub fn rule_hash(&self) -> Option<[u8; 32]> {
451 self.cell.get()
452 }
453
454 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 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 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 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 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 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 pub fn overlay_path(&self) -> Option<PathBuf> {
603 self.paths.overlay()
604 }
605}
606
607static 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
637pub 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}