1use std::env;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use std::time::{Duration, Instant};
12
13use lean_rs_worker_protocol::types::{
14 LeanWorkerCapabilityMetadata, LeanWorkerDeclarationInspectionRequest, LeanWorkerDeclarationInspectionResult,
15 LeanWorkerDeclarationSearch, LeanWorkerDeclarationSearchResult, LeanWorkerDeclarationVerificationBatchRequest,
16 LeanWorkerDeclarationVerificationBatchResult, LeanWorkerDeclarationVerificationRequest,
17 LeanWorkerDeclarationVerificationResult, LeanWorkerElabOptions, LeanWorkerModuleQuery,
18 LeanWorkerModuleQueryBatchOutcome, LeanWorkerModuleQueryOutcome, LeanWorkerModuleQuerySelector,
19 LeanWorkerOutputBudgets, LeanWorkerProofAttemptRequest, LeanWorkerProofAttemptResult,
20 LeanWorkerSessionImportProfile,
21};
22use lean_rs_worker_protocol::worker_exports::{
23 doctor_signature, json_command_signature, metadata_signature, streaming_command_signature,
24};
25use lean_toolchain::{LeanBuiltCapability, LeanExportSignature, LeanLoaderDiagnosticCode};
26use serde::Deserialize;
27use serde_json::Value;
28
29use crate::pool::{LeanWorkerRestartPolicyClass, LeanWorkerSessionKey};
30use crate::session::{
31 LeanWorkerCancellationToken, LeanWorkerProgressSink, LeanWorkerRuntimeMetadata, LeanWorkerSession,
32 LeanWorkerSessionConfig,
33};
34use crate::supervisor::{
35 LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING, LeanWorker, LeanWorkerConfig, LeanWorkerError,
36 LeanWorkerLifecycleSnapshot, LeanWorkerRestartPolicy, LeanWorkerRestartReason, LeanWorkerShutdownReport,
37 LeanWorkerStats, LeanWorkerStatus,
38};
39
40const WORKER_CHILD_ENV: &str = "LEAN_RS_WORKER_CHILD";
41
42#[derive(Clone, Debug)]
62pub struct LeanWorkerCapabilityBuilder {
63 project_root: PathBuf,
64 import_workspace_root: Option<PathBuf>,
65 package: String,
66 lib_name: String,
67 imports: Vec<String>,
68 import_profile: LeanWorkerSessionImportProfile,
69 built_dylib_path: Option<PathBuf>,
70 built_manifest_path: Option<PathBuf>,
71 built_capability: Option<LeanBuiltCapability>,
72 worker_child: Option<LeanWorkerChild>,
73 startup_timeout: Option<Duration>,
74 request_timeout: Option<Duration>,
75 shutdown_timeout: Option<Duration>,
76 restart_policy: Option<LeanWorkerRestartPolicy>,
77 rss_hard_limit: Option<(u64, Duration)>,
78 module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
79 num_threads: Option<u32>,
80 lean_max_memory_kib: Option<u64>,
81 metadata_check: Option<CapabilityMetadataCheck>,
82 max_frame_bytes: Option<u32>,
83 worker_export_signatures: Vec<LeanExportSignature>,
84}
85
86impl LeanWorkerCapabilityBuilder {
87 #[must_use]
96 pub fn new(
97 project_root: impl Into<PathBuf>,
98 package: impl Into<String>,
99 lib_name: impl Into<String>,
100 imports: impl IntoIterator<Item = impl Into<String>>,
101 ) -> Self {
102 Self {
103 project_root: project_root.into(),
104 import_workspace_root: None,
105 package: package.into(),
106 lib_name: lib_name.into(),
107 imports: imports.into_iter().map(Into::into).collect(),
108 import_profile: LeanWorkerSessionImportProfile::default(),
109 built_dylib_path: None,
110 built_manifest_path: None,
111 built_capability: None,
112 worker_child: None,
113 startup_timeout: None,
114 request_timeout: None,
115 shutdown_timeout: None,
116 restart_policy: None,
117 rss_hard_limit: None,
118 module_cache_limits: None,
119 num_threads: None,
120 lean_max_memory_kib: None,
121 metadata_check: None,
122 max_frame_bytes: None,
123 worker_export_signatures: Vec::new(),
124 }
125 }
126
127 pub fn from_built_capability(
144 spec: &LeanBuiltCapability,
145 imports: impl IntoIterator<Item = impl Into<String>>,
146 ) -> Result<Self, LeanWorkerError> {
147 let artifact = WorkerCapabilityArtifact::from_built_capability(spec)?;
148 let project_root = infer_lake_project_root_from_dylib(&artifact.dylib_path)?;
149 Ok(Self {
150 project_root,
151 import_workspace_root: None,
152 package: artifact.package,
153 lib_name: artifact.module,
154 imports: imports.into_iter().map(Into::into).collect(),
155 import_profile: LeanWorkerSessionImportProfile::default(),
156 built_dylib_path: Some(artifact.dylib_path),
157 built_manifest_path: artifact.manifest_path,
158 built_capability: Some(spec.clone()),
159 worker_child: None,
160 startup_timeout: None,
161 request_timeout: None,
162 shutdown_timeout: None,
163 restart_policy: None,
164 rss_hard_limit: None,
165 module_cache_limits: None,
166 num_threads: None,
167 lean_max_memory_kib: None,
168 metadata_check: None,
169 max_frame_bytes: None,
170 worker_export_signatures: Vec::new(),
171 })
172 }
173
174 #[must_use]
179 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
180 self.worker_child = Some(LeanWorkerChild::path(path));
181 self
182 }
183
184 #[must_use]
186 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
187 self.worker_child = Some(child);
188 self
189 }
190
191 #[must_use]
208 pub fn import_workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
209 self.import_workspace_root = Some(normalize_import_workspace_root(path.into()));
210 self
211 }
212
213 #[must_use]
215 pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
216 self.import_profile = profile;
217 self
218 }
219
220 #[must_use]
222 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
223 self.startup_timeout = Some(timeout);
224 self
225 }
226
227 #[must_use]
229 pub fn request_timeout(mut self, timeout: Duration) -> Self {
230 self.request_timeout = Some(timeout);
231 self
232 }
233
234 #[must_use]
236 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
237 self.shutdown_timeout = Some(timeout);
238 self
239 }
240
241 #[must_use]
243 pub fn long_running_requests(mut self) -> Self {
244 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
245 self
246 }
247
248 #[must_use]
250 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
251 self.restart_policy = Some(policy);
252 self
253 }
254
255 #[must_use]
258 pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
259 self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
260 self
261 }
262
263 #[must_use]
269 pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
270 self.module_cache_limits = Some(limits);
271 self
272 }
273
274 #[must_use]
281 pub fn num_threads(mut self, threads: u32) -> Self {
282 self.num_threads = Some(threads);
283 self
284 }
285
286 #[must_use]
292 pub fn lean_max_memory_kib(mut self, limit_kib: u64) -> Self {
293 self.lean_max_memory_kib = Some(limit_kib);
294 self
295 }
296
297 #[must_use]
305 pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
306 self.max_frame_bytes = Some(max_frame_bytes);
307 self
308 }
309
310 #[must_use]
316 pub fn validate_metadata(mut self, export: impl Into<String>, request: Value) -> Self {
317 let export = export.into();
318 self.add_worker_export_signature(metadata_signature(export.clone()));
319 self.metadata_check = Some(CapabilityMetadataCheck {
320 export,
321 request,
322 expected: None,
323 });
324 self
325 }
326
327 #[must_use]
333 pub fn expect_metadata(
334 mut self,
335 export: impl Into<String>,
336 request: Value,
337 expected: LeanWorkerCapabilityMetadata,
338 ) -> Self {
339 let export = export.into();
340 self.add_worker_export_signature(metadata_signature(export.clone()));
341 self.metadata_check = Some(CapabilityMetadataCheck {
342 export,
343 request,
344 expected: Some(expected),
345 });
346 self
347 }
348
349 #[must_use]
351 pub fn metadata_export(mut self, export: impl Into<String>) -> Self {
352 self.add_worker_export_signature(metadata_signature(export));
353 self
354 }
355
356 #[must_use]
358 pub fn doctor_export(mut self, export: impl Into<String>) -> Self {
359 self.add_worker_export_signature(doctor_signature(export));
360 self
361 }
362
363 #[must_use]
365 pub fn json_command_export(mut self, export: impl Into<String>) -> Self {
366 self.add_worker_export_signature(json_command_signature(export));
367 self
368 }
369
370 #[must_use]
372 pub fn streaming_command_export(mut self, export: impl Into<String>) -> Self {
373 self.add_worker_export_signature(streaming_command_signature(export));
374 self
375 }
376
377 fn add_worker_export_signature(&mut self, signature: LeanExportSignature) {
378 if self
379 .worker_export_signatures
380 .iter()
381 .all(|existing| existing.symbol() != signature.symbol())
382 {
383 self.worker_export_signatures.push(signature);
384 }
385 }
386
387 #[must_use]
393 pub fn session_key(&self) -> LeanWorkerSessionKey {
394 let restart_policy_class = match &self.restart_policy {
395 Some(policy) if policy == &LeanWorkerRestartPolicy::default() => LeanWorkerRestartPolicyClass::Default,
396 Some(_policy) => LeanWorkerRestartPolicyClass::Custom,
397 None => LeanWorkerRestartPolicyClass::Default,
398 };
399 let mut key = LeanWorkerSessionKey::new(
400 self.project_root.clone(),
401 self.package.clone(),
402 self.lib_name.clone(),
403 self.imports.clone(),
404 )
405 .with_import_profile(self.import_profile)
406 .with_import_workspace_root(self.effective_import_workspace_root())
407 .restart_policy_class(restart_policy_class);
408 if let Some(manifest_path) = &self.built_manifest_path {
409 key = key.with_built_manifest_path(manifest_path.clone());
410 }
411 if let Some(check) = &self.metadata_check {
412 key = key.metadata_expectation(check.export.clone(), check.request.clone(), check.expected.clone());
413 }
414 key
415 }
416
417 fn effective_import_workspace_root(&self) -> PathBuf {
418 self.import_workspace_root
419 .clone()
420 .unwrap_or_else(|| normalize_import_workspace_root(self.project_root.clone()))
421 }
422
423 pub(crate) fn pool_request_timeout(&self) -> Duration {
424 self.request_timeout
425 .unwrap_or(crate::supervisor::LEAN_WORKER_REQUEST_TIMEOUT_DEFAULT)
426 }
427
428 #[must_use]
436 pub fn check(&self) -> LeanWorkerBootstrapReport {
437 let mut checks = self.bootstrap_static_checks();
438 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
439 return LeanWorkerBootstrapReport::new(checks);
440 }
441
442 match self.clone().open_unchecked() {
443 Ok(capability) => {
444 drop(capability.shutdown());
445 }
446 Err(err) => checks.push(check_from_open_error(&err)),
447 }
448 LeanWorkerBootstrapReport::new(checks)
449 }
450
451 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
452 let mut checks = Vec::new();
453 checks.extend(worker_child_static_checks(self.worker_child.as_ref()));
454
455 if let Some(spec) = &self.built_capability
456 && let Ok(manifest_path) = spec.resolved_manifest_path()
457 {
458 let report = lean_toolchain::manifest_validation::check_static(&manifest_path);
459 for check in report.errors() {
460 checks.push(LeanWorkerBootstrapCheck::error(
461 LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight { code: check.code() },
462 check.subject().to_owned(),
463 check.message().to_owned(),
464 check.repair_hint().to_owned(),
465 ));
466 }
467 }
468 checks
469 }
470
471 pub fn open(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
479 let report = self.bootstrap_static_report();
480 if let Some(check) = report.first_error() {
481 return Err(LeanWorkerError::Bootstrap {
482 code: check.code(),
483 message: check.message().to_owned(),
484 });
485 }
486 self.open_unchecked()
487 }
488
489 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
490 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
491 }
492
493 fn open_unchecked(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
494 let import_workspace_root = self.effective_import_workspace_root();
495 let capability_load_started = Instant::now();
496 let (dylib_path, manifest_path) = match (self.built_dylib_path, self.built_manifest_path) {
497 (Some(dylib_path), Some(manifest_path)) => (dylib_path, manifest_path),
498 (_, None) => {
499 let mut builder = lean_toolchain::CargoLeanCapability::new(&self.project_root, &self.lib_name)
500 .package(&self.package)
501 .module(&self.lib_name);
502 for signature in self.worker_export_signatures {
503 builder = builder.export_signature(signature);
504 }
505 let built = builder
506 .build_quiet()
507 .map_err(|diagnostic| LeanWorkerError::CapabilityBuild { diagnostic })?;
508 (built.dylib_path().to_path_buf(), built.manifest_path().to_path_buf())
509 }
510 (None, Some(manifest_path)) => {
511 let artifact = WorkerCapabilityArtifact::from_manifest(&manifest_path)?;
512 (artifact.dylib_path, manifest_path)
513 }
514 };
515 let capability_load_elapsed = capability_load_started.elapsed();
516 let mut worker = spawn_checked_worker(
517 self.worker_child,
518 self.startup_timeout,
519 self.request_timeout,
520 self.shutdown_timeout,
521 self.restart_policy,
522 self.rss_hard_limit,
523 self.module_cache_limits,
524 self.max_frame_bytes,
525 self.num_threads,
526 self.lean_max_memory_kib,
527 )?;
528
529 let session_config = LeanWorkerSessionConfig::manifest_backed(
530 import_workspace_root,
531 self.package.clone(),
532 self.lib_name.clone(),
533 manifest_path,
534 self.imports.clone(),
535 )
536 .with_import_profile(self.import_profile);
537
538 let session_open_import_elapsed;
539 let validated_metadata = {
540 let session_open_started = Instant::now();
541 let mut session = worker.open_session(&session_config, None, None)?;
542 session_open_import_elapsed = session_open_started.elapsed();
543 match self.metadata_check {
544 Some(check) => {
545 let metadata = session.capability_metadata(&check.export, &check.request, None, None)?;
546 if let Some(expected) = check.expected
547 && metadata != expected
548 {
549 return Err(LeanWorkerError::CapabilityMetadataMismatch {
550 export: check.export,
551 expected: Box::new(expected),
552 actual: Box::new(metadata),
553 });
554 }
555 Some(metadata)
556 }
557 None => None,
558 }
559 };
560 worker.record_capability_open_timing(capability_load_elapsed, session_open_import_elapsed);
561
562 Ok(LeanWorkerCapability {
563 worker,
564 session_config,
565 dylib_path,
566 validated_metadata,
567 })
568 }
569}
570
571#[derive(Clone, Debug)]
579pub struct LeanWorkerHostHandleBuilder {
580 project_root: PathBuf,
581 imports: Vec<String>,
582 import_profile: LeanWorkerSessionImportProfile,
583 worker_child: Option<LeanWorkerChild>,
584 startup_timeout: Option<Duration>,
585 request_timeout: Option<Duration>,
586 shutdown_timeout: Option<Duration>,
587 restart_policy: Option<LeanWorkerRestartPolicy>,
588 rss_hard_limit: Option<(u64, Duration)>,
589 module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
590 num_threads: Option<u32>,
591 lean_max_memory_kib: Option<u64>,
592 max_frame_bytes: Option<u32>,
593}
594
595#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
601pub struct LeanWorkerModuleCacheLimits {
602 max_entries: Option<u64>,
603 ttl_millis: Option<u64>,
604 max_bytes: Option<u64>,
605 rss_guard_kib: Option<u64>,
606 verify_rss_taint_kib: Option<u64>,
607}
608
609impl LeanWorkerModuleCacheLimits {
610 #[must_use]
612 pub fn max_entries(mut self, max_entries: u64) -> Self {
613 self.max_entries = Some(max_entries.max(1));
614 self
615 }
616
617 #[must_use]
619 pub fn ttl(mut self, ttl: Duration) -> Self {
620 self.ttl_millis = Some(u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX).max(1));
621 self
622 }
623
624 #[must_use]
626 pub fn max_bytes(mut self, max_bytes: u64) -> Self {
627 self.max_bytes = Some(max_bytes.max(1));
628 self
629 }
630
631 #[must_use]
634 pub fn rss_guard_kib(mut self, rss_guard_kib: u64) -> Self {
635 self.rss_guard_kib = Some(rss_guard_kib.max(1));
636 self
637 }
638
639 #[must_use]
646 pub fn verify_rss_taint_kib(mut self, verify_rss_taint_kib: u64) -> Self {
647 self.verify_rss_taint_kib = Some(verify_rss_taint_kib.max(1));
648 self
649 }
650}
651
652impl LeanWorkerHostHandleBuilder {
653 #[must_use]
659 pub fn shims_only(project_root: impl Into<PathBuf>, imports: impl IntoIterator<Item = impl Into<String>>) -> Self {
660 Self {
661 project_root: project_root.into(),
662 imports: imports.into_iter().map(Into::into).collect(),
663 import_profile: LeanWorkerSessionImportProfile::default(),
664 worker_child: None,
665 startup_timeout: None,
666 request_timeout: None,
667 shutdown_timeout: None,
668 restart_policy: None,
669 rss_hard_limit: None,
670 module_cache_limits: None,
671 num_threads: None,
672 lean_max_memory_kib: None,
673 max_frame_bytes: None,
674 }
675 }
676
677 #[must_use]
679 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
680 self.worker_child = Some(LeanWorkerChild::path(path));
681 self
682 }
683
684 #[must_use]
686 pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
687 self.import_profile = profile;
688 self
689 }
690
691 #[must_use]
693 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
694 self.worker_child = Some(child);
695 self
696 }
697
698 #[must_use]
700 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
701 self.startup_timeout = Some(timeout);
702 self
703 }
704
705 #[must_use]
707 pub fn request_timeout(mut self, timeout: Duration) -> Self {
708 self.request_timeout = Some(timeout);
709 self
710 }
711
712 #[must_use]
714 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
715 self.shutdown_timeout = Some(timeout);
716 self
717 }
718
719 #[must_use]
721 pub fn long_running_requests(mut self) -> Self {
722 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
723 self
724 }
725
726 #[must_use]
728 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
729 self.restart_policy = Some(policy);
730 self
731 }
732
733 #[must_use]
736 pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
737 self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
738 self
739 }
740
741 #[must_use]
746 pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
747 self.module_cache_limits = Some(limits);
748 self
749 }
750
751 #[must_use]
755 pub fn num_threads(mut self, threads: u32) -> Self {
756 self.num_threads = Some(threads);
757 self
758 }
759
760 #[must_use]
764 pub fn lean_max_memory_kib(mut self, limit_kib: u64) -> Self {
765 self.lean_max_memory_kib = Some(limit_kib);
766 self
767 }
768
769 #[must_use]
771 pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
772 self.max_frame_bytes = Some(max_frame_bytes);
773 self
774 }
775
776 #[must_use]
781 pub fn check(&self) -> LeanWorkerBootstrapReport {
782 let mut checks = self.bootstrap_static_checks();
783 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
784 return LeanWorkerBootstrapReport::new(checks);
785 }
786
787 match self.clone().open_unchecked() {
788 Ok(handle) => {
789 drop(handle.shutdown());
790 }
791 Err(err) => checks.push(check_from_open_error(&err)),
792 }
793 LeanWorkerBootstrapReport::new(checks)
794 }
795
796 pub fn open(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
804 let report = self.bootstrap_static_report();
805 if let Some(check) = report.first_error() {
806 return Err(LeanWorkerError::Bootstrap {
807 code: check.code(),
808 message: check.message().to_owned(),
809 });
810 }
811 self.open_unchecked()
812 }
813
814 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
815 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
816 }
817
818 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
819 worker_child_static_checks(self.worker_child.as_ref())
820 }
821
822 fn open_unchecked(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
823 let mut worker = spawn_checked_worker(
824 self.worker_child,
825 self.startup_timeout,
826 self.request_timeout,
827 self.shutdown_timeout,
828 self.restart_policy,
829 self.rss_hard_limit,
830 self.module_cache_limits,
831 self.max_frame_bytes,
832 self.num_threads,
833 self.lean_max_memory_kib,
834 )?;
835 let session_config = LeanWorkerSessionConfig::shims_only(self.project_root, self.imports)
836 .with_import_profile(self.import_profile);
837 {
838 let _session = worker.open_session(&session_config, None, None)?;
839 }
840 Ok(LeanWorkerHostHandle { worker, session_config })
841 }
842}
843
844#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
846pub enum LeanWorkerBootstrapDiagnosticCode {
847 WorkerChildUnresolved,
849 WorkerChildNotExecutable,
851 CapabilityPreflight { code: LeanLoaderDiagnosticCode },
853 WorkerHandshakeFailed,
855 CapabilityMetadataMismatch,
857 WorkerStartupFailed,
859}
860
861impl LeanWorkerBootstrapDiagnosticCode {
862 #[must_use]
864 pub const fn as_str(self) -> &'static str {
865 match self {
866 Self::WorkerChildUnresolved => "lean_rs.worker.bootstrap.child_unresolved",
867 Self::WorkerChildNotExecutable => "lean_rs.worker.bootstrap.child_not_executable",
868 Self::CapabilityPreflight { code } => code.as_str(),
869 Self::WorkerHandshakeFailed => "lean_rs.worker.bootstrap.handshake_failed",
870 Self::CapabilityMetadataMismatch => "lean_rs.worker.bootstrap.metadata_mismatch",
871 Self::WorkerStartupFailed => "lean_rs.worker.bootstrap.startup_failed",
872 }
873 }
874}
875
876impl std::fmt::Display for LeanWorkerBootstrapDiagnosticCode {
877 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878 f.write_str(self.as_str())
879 }
880}
881
882#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
884pub enum LeanWorkerBootstrapSeverity {
885 Info,
887 Warning,
889 Error,
891}
892
893#[derive(Clone, Debug, Eq, PartialEq)]
895pub struct LeanWorkerBootstrapCheck {
896 code: LeanWorkerBootstrapDiagnosticCode,
897 severity: LeanWorkerBootstrapSeverity,
898 subject: String,
899 message: String,
900 repair_hint: String,
901}
902
903impl LeanWorkerBootstrapCheck {
904 fn error(
905 code: LeanWorkerBootstrapDiagnosticCode,
906 subject: impl Into<String>,
907 message: impl Into<String>,
908 repair_hint: impl Into<String>,
909 ) -> Self {
910 Self {
911 code,
912 severity: LeanWorkerBootstrapSeverity::Error,
913 subject: bound_bootstrap_text(subject.into()),
914 message: bound_bootstrap_text(message.into()),
915 repair_hint: bound_bootstrap_text(repair_hint.into()),
916 }
917 }
918
919 #[must_use]
921 pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
922 self.code
923 }
924
925 #[must_use]
927 pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
928 self.severity
929 }
930
931 #[must_use]
933 pub fn subject(&self) -> &str {
934 &self.subject
935 }
936
937 #[must_use]
939 pub fn message(&self) -> &str {
940 &self.message
941 }
942
943 #[must_use]
945 pub fn repair_hint(&self) -> &str {
946 &self.repair_hint
947 }
948
949 fn is_error(&self) -> bool {
950 self.severity == LeanWorkerBootstrapSeverity::Error
951 }
952}
953
954#[derive(Clone, Debug, Eq, PartialEq)]
956pub struct LeanWorkerBootstrapReport {
957 checks: Vec<LeanWorkerBootstrapCheck>,
958}
959
960impl LeanWorkerBootstrapReport {
961 fn new(checks: Vec<LeanWorkerBootstrapCheck>) -> Self {
962 Self { checks }
963 }
964
965 #[must_use]
967 pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
968 &self.checks
969 }
970
971 pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
973 self.checks
974 .iter()
975 .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
976 }
977
978 #[must_use]
980 pub fn is_ok(&self) -> bool {
981 self.first_error().is_none()
982 }
983
984 #[must_use]
986 pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
987 self.errors().next()
988 }
989}
990
991#[derive(Debug)]
997pub struct LeanWorkerCapability {
998 worker: LeanWorker,
999 session_config: LeanWorkerSessionConfig,
1000 dylib_path: PathBuf,
1001 validated_metadata: Option<LeanWorkerCapabilityMetadata>,
1002}
1003
1004impl LeanWorkerCapability {
1005 pub fn open_session(
1017 &mut self,
1018 cancellation: Option<&LeanWorkerCancellationToken>,
1019 progress: Option<&dyn LeanWorkerProgressSink>,
1020 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1021 self.worker.open_session(&self.session_config, cancellation, progress)
1022 }
1023
1024 pub(crate) fn attach_open_session(&mut self) -> LeanWorkerSession<'_> {
1025 self.worker.attach_open_session()
1026 }
1027
1028 pub fn open_session_with_imports(
1039 &mut self,
1040 imports: impl IntoIterator<Item = impl Into<String>>,
1041 cancellation: Option<&LeanWorkerCancellationToken>,
1042 progress: Option<&dyn LeanWorkerProgressSink>,
1043 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1044 let config = self.session_config.with_imports(imports);
1045 self.worker.open_session(&config, cancellation, progress)
1046 }
1047
1048 #[must_use]
1050 pub fn dylib_path(&self) -> &Path {
1051 &self.dylib_path
1052 }
1053
1054 #[must_use]
1056 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1057 &self.session_config
1058 }
1059
1060 #[must_use]
1062 pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
1063 self.validated_metadata.as_ref()
1064 }
1065
1066 #[must_use]
1068 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1069 self.worker.runtime_metadata()
1070 }
1071
1072 #[must_use]
1074 pub fn stats(&self) -> LeanWorkerStats {
1075 self.worker.stats()
1076 }
1077
1078 #[must_use]
1080 pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1081 self.worker.lifecycle_snapshot()
1082 }
1083
1084 pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1090 self.worker.status()
1091 }
1092
1093 pub fn rss_kib(&mut self) -> Option<u64> {
1095 self.worker.rss_kib()
1096 }
1097
1098 pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1104 self.worker.cycle()
1105 }
1106
1107 pub(crate) fn cycle_with_restart_reason(&mut self, reason: LeanWorkerRestartReason) -> Result<(), LeanWorkerError> {
1108 self.worker.cycle_with_restart_reason(reason)
1109 }
1110
1111 pub(crate) fn record_command_timing(&mut self, first_command_after_open: bool, elapsed: Duration) {
1112 self.worker.record_command_timing(first_command_after_open, elapsed);
1113 }
1114
1115 pub fn set_request_timeout(&mut self, timeout: Duration) {
1117 self.worker.set_request_timeout(timeout);
1118 }
1119
1120 #[doc(hidden)]
1121 pub fn __kill_for_test(&mut self) -> Result<(), LeanWorkerError> {
1127 self.worker.__kill_for_test()
1128 }
1129
1130 #[deprecated(note = "use LeanWorkerCapability::shutdown for structured shutdown status")]
1137 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1138 self.worker.shutdown().map(|report| report.exit)
1139 }
1140
1141 pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1147 self.worker.shutdown()
1148 }
1149}
1150
1151#[derive(Debug)]
1158pub struct LeanWorkerHostHandle {
1159 worker: LeanWorker,
1160 session_config: LeanWorkerSessionConfig,
1161}
1162
1163impl LeanWorkerHostHandle {
1164 pub fn open_session(
1176 &mut self,
1177 cancellation: Option<&LeanWorkerCancellationToken>,
1178 progress: Option<&dyn LeanWorkerProgressSink>,
1179 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1180 self.worker.open_session(&self.session_config, cancellation, progress)
1181 }
1182
1183 pub fn open_session_with_imports(
1193 &mut self,
1194 imports: impl IntoIterator<Item = impl Into<String>>,
1195 cancellation: Option<&LeanWorkerCancellationToken>,
1196 progress: Option<&dyn LeanWorkerProgressSink>,
1197 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1198 let config = self.session_config.with_imports(imports);
1199 self.worker.open_session(&config, cancellation, progress)
1200 }
1201
1202 fn with_session_imports<T>(
1203 &mut self,
1204 imports: Vec<String>,
1205 cancellation: Option<&LeanWorkerCancellationToken>,
1206 progress: Option<&dyn LeanWorkerProgressSink>,
1207 command: impl Fn(&mut LeanWorkerSession<'_>) -> Result<T, LeanWorkerError>,
1208 ) -> Result<T, LeanWorkerError> {
1209 let result = {
1210 let mut session = self.open_session_with_imports(imports.clone(), cancellation, progress)?;
1211 command(&mut session)
1212 };
1213 match result {
1214 Ok(value) => Ok(value),
1215 Err(err) if worker_session_missing(&err) => {
1216 let mut session = self.open_session_with_imports(imports, cancellation, progress)?;
1217 command(&mut session)
1218 }
1219 Err(err) => Err(err),
1220 }
1221 }
1222
1223 pub fn process_module_query_with_imports(
1233 &mut self,
1234 imports: Vec<String>,
1235 source: &str,
1236 query: &LeanWorkerModuleQuery,
1237 options: &LeanWorkerElabOptions,
1238 cancellation: Option<&LeanWorkerCancellationToken>,
1239 progress: Option<&dyn LeanWorkerProgressSink>,
1240 ) -> Result<LeanWorkerModuleQueryOutcome, LeanWorkerError> {
1241 self.with_session_imports(imports, cancellation, progress, |session| {
1242 session.process_module_query(source, query.clone(), options, cancellation, progress)
1243 })
1244 }
1245
1246 pub fn process_module_query_batch_with_imports(
1253 &mut self,
1254 imports: Vec<String>,
1255 source: &str,
1256 selectors: &[LeanWorkerModuleQuerySelector],
1257 budgets: &LeanWorkerOutputBudgets,
1258 options: &LeanWorkerElabOptions,
1259 cancellation: Option<&LeanWorkerCancellationToken>,
1260 progress: Option<&dyn LeanWorkerProgressSink>,
1261 ) -> Result<LeanWorkerModuleQueryBatchOutcome, LeanWorkerError> {
1262 self.with_session_imports(imports, cancellation, progress, |session| {
1263 session.process_module_query_batch(source, selectors, budgets, options, cancellation, progress)
1264 })
1265 }
1266
1267 pub fn inspect_declaration_with_imports(
1273 &mut self,
1274 imports: Vec<String>,
1275 request: &LeanWorkerDeclarationInspectionRequest,
1276 cancellation: Option<&LeanWorkerCancellationToken>,
1277 progress: Option<&dyn LeanWorkerProgressSink>,
1278 ) -> Result<LeanWorkerDeclarationInspectionResult, LeanWorkerError> {
1279 self.with_session_imports(imports, cancellation, progress, |session| {
1280 session.inspect_declaration(request, cancellation, progress)
1281 })
1282 }
1283
1284 pub fn search_declarations_with_imports(
1290 &mut self,
1291 imports: Vec<String>,
1292 search: &LeanWorkerDeclarationSearch,
1293 cancellation: Option<&LeanWorkerCancellationToken>,
1294 progress: Option<&dyn LeanWorkerProgressSink>,
1295 ) -> Result<LeanWorkerDeclarationSearchResult, LeanWorkerError> {
1296 self.with_session_imports(imports, cancellation, progress, |session| {
1297 session.search_declarations(search, cancellation, progress)
1298 })
1299 }
1300
1301 pub fn attempt_proof_with_imports(
1307 &mut self,
1308 imports: Vec<String>,
1309 request: &LeanWorkerProofAttemptRequest,
1310 options: &LeanWorkerElabOptions,
1311 cancellation: Option<&LeanWorkerCancellationToken>,
1312 progress: Option<&dyn LeanWorkerProgressSink>,
1313 ) -> Result<LeanWorkerProofAttemptResult, LeanWorkerError> {
1314 self.with_session_imports(imports, cancellation, progress, |session| {
1315 session.attempt_proof(request, options, cancellation, progress)
1316 })
1317 }
1318
1319 pub fn verify_declaration_with_imports(
1325 &mut self,
1326 imports: Vec<String>,
1327 request: &LeanWorkerDeclarationVerificationRequest,
1328 options: &LeanWorkerElabOptions,
1329 cancellation: Option<&LeanWorkerCancellationToken>,
1330 progress: Option<&dyn LeanWorkerProgressSink>,
1331 ) -> Result<LeanWorkerDeclarationVerificationResult, LeanWorkerError> {
1332 self.with_session_imports(imports, cancellation, progress, |session| {
1333 session.verify_declaration(request, options, cancellation, progress)
1334 })
1335 }
1336
1337 pub fn verify_declaration_batch_with_imports(
1344 &mut self,
1345 imports: Vec<String>,
1346 request: &LeanWorkerDeclarationVerificationBatchRequest,
1347 options: &LeanWorkerElabOptions,
1348 cancellation: Option<&LeanWorkerCancellationToken>,
1349 progress: Option<&dyn LeanWorkerProgressSink>,
1350 ) -> Result<LeanWorkerDeclarationVerificationBatchResult, LeanWorkerError> {
1351 self.with_session_imports(imports, cancellation, progress, |session| {
1352 session.verify_declaration_batch(request, options, cancellation, progress)
1353 })
1354 }
1355
1356 #[must_use]
1358 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1359 &self.session_config
1360 }
1361
1362 #[must_use]
1364 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1365 self.worker.runtime_metadata()
1366 }
1367
1368 #[must_use]
1370 pub fn stats(&self) -> LeanWorkerStats {
1371 self.worker.stats()
1372 }
1373
1374 #[must_use]
1376 pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1377 self.worker.lifecycle_snapshot()
1378 }
1379
1380 pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1386 self.worker.status()
1387 }
1388
1389 pub fn rss_kib(&mut self) -> Option<u64> {
1391 self.worker.rss_kib()
1392 }
1393
1394 pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1400 self.worker.cycle()
1401 }
1402
1403 pub fn restart(&mut self) -> Result<(), LeanWorkerError> {
1409 self.worker.restart()
1410 }
1411
1412 #[deprecated(note = "use LeanWorkerHostHandle::shutdown for structured shutdown status")]
1419 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1420 self.worker.shutdown().map(|report| report.exit)
1421 }
1422
1423 pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1429 self.worker.shutdown()
1430 }
1431}
1432
1433fn worker_session_missing(err: &LeanWorkerError) -> bool {
1434 matches!(err, LeanWorkerError::Worker { code, .. } if code == "lean_rs.worker.session_missing")
1435}
1436
1437#[derive(Clone, Debug)]
1438struct CapabilityMetadataCheck {
1439 export: String,
1440 request: Value,
1441 expected: Option<LeanWorkerCapabilityMetadata>,
1442}
1443
1444#[derive(Debug)]
1445struct WorkerCapabilityArtifact {
1446 dylib_path: PathBuf,
1447 manifest_path: Option<PathBuf>,
1448 package: String,
1449 module: String,
1450}
1451
1452impl WorkerCapabilityArtifact {
1453 fn from_built_capability(spec: &LeanBuiltCapability) -> Result<Self, LeanWorkerError> {
1454 if let Ok(manifest_path) = spec.resolved_manifest_path() {
1455 let mut artifact = Self::from_manifest(&manifest_path)?;
1456 artifact.manifest_path = Some(manifest_path);
1457 return Ok(artifact);
1458 }
1459
1460 let dylib_path = spec.dylib_path().map_err(|err| LeanWorkerError::Setup {
1461 message: err.to_string(),
1462 })?;
1463 let package = spec.package_name().ok_or_else(|| LeanWorkerError::Setup {
1464 message: "LeanBuiltCapability is missing the Lake package name; call `.package(...)`".to_owned(),
1465 })?;
1466 let module = spec.module_name().ok_or_else(|| LeanWorkerError::Setup {
1467 message: "LeanBuiltCapability is missing the root Lean module name; call `.module(...)`".to_owned(),
1468 })?;
1469 Ok(Self {
1470 dylib_path,
1471 manifest_path: None,
1472 package: package.to_owned(),
1473 module: module.to_owned(),
1474 })
1475 }
1476
1477 fn from_manifest(manifest_path: &Path) -> Result<Self, LeanWorkerError> {
1478 let bytes = std::fs::read(manifest_path).map_err(|err| LeanWorkerError::Bootstrap {
1479 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1480 code: LeanLoaderDiagnosticCode::MissingManifest,
1481 },
1482 message: format!(
1483 "could not read Lean capability manifest '{}': {err}",
1484 manifest_path.display()
1485 ),
1486 })?;
1487 let manifest: WorkerCapabilityManifest =
1488 serde_json::from_slice(&bytes).map_err(|err| LeanWorkerError::Bootstrap {
1489 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1490 code: LeanLoaderDiagnosticCode::MalformedManifest,
1491 },
1492 message: format!(
1493 "Lean capability manifest '{}' is malformed: {err}",
1494 manifest_path.display()
1495 ),
1496 })?;
1497 if manifest.schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
1498 return Err(LeanWorkerError::Bootstrap {
1499 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1500 code: LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
1501 },
1502 message: format!(
1503 "unsupported Lean capability manifest schema {}; supported schema is {}",
1504 manifest.schema_version,
1505 lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
1506 ),
1507 });
1508 }
1509 Ok(Self {
1510 dylib_path: manifest.primary_dylib,
1511 manifest_path: Some(manifest_path.to_path_buf()),
1512 package: manifest.package,
1513 module: manifest.module,
1514 })
1515 }
1516}
1517
1518#[derive(Deserialize)]
1519struct WorkerCapabilityManifest {
1520 schema_version: u64,
1521 primary_dylib: PathBuf,
1522 package: String,
1523 module: String,
1524}
1525
1526fn worker_child_static_checks(worker_child: Option<&LeanWorkerChild>) -> Vec<LeanWorkerBootstrapCheck> {
1527 let mut checks = Vec::new();
1528 match worker_child.map_or_else(resolve_default_worker_executable, LeanWorkerChild::resolve) {
1529 Ok(path) => {
1530 if let Err(err) = validate_worker_child_path(&path) {
1531 checks.push(check_from_open_error(&err));
1532 }
1533 }
1534 Err(err) => checks.push(check_from_open_error(&err)),
1535 }
1536 checks
1537}
1538
1539fn spawn_checked_worker(
1540 worker_child: Option<LeanWorkerChild>,
1541 startup_timeout: Option<Duration>,
1542 request_timeout: Option<Duration>,
1543 shutdown_timeout: Option<Duration>,
1544 restart_policy: Option<LeanWorkerRestartPolicy>,
1545 rss_hard_limit: Option<(u64, Duration)>,
1546 module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
1547 max_frame_bytes: Option<u32>,
1548 num_threads: Option<u32>,
1549 lean_max_memory_kib: Option<u64>,
1550) -> Result<LeanWorker, LeanWorkerError> {
1551 let worker_child = worker_child.unwrap_or_default();
1552 let worker_executable = worker_child.resolve()?;
1553 validate_worker_child_path(&worker_executable)?;
1554 let lean_sysroot = worker_child.resolve_lean_sysroot()?;
1555
1556 let mut config = LeanWorkerConfig::new(worker_executable).env("LEAN_SYSROOT", lean_sysroot.as_os_str());
1557 if let Some(timeout) = startup_timeout {
1558 config = config.startup_timeout(timeout);
1559 }
1560 if let Some(timeout) = request_timeout {
1561 config = config.request_timeout(timeout);
1562 }
1563 if let Some(timeout) = shutdown_timeout {
1564 config = config.shutdown_timeout(timeout);
1565 }
1566 if let Some(policy) = restart_policy {
1567 config = config.restart_policy(policy);
1568 }
1569 if let Some((limit_kib, sample_interval)) = rss_hard_limit {
1570 config = config.rss_hard_limit(limit_kib, sample_interval);
1571 }
1572 if let Some(limits) = module_cache_limits.as_ref() {
1573 config = apply_module_cache_limits(config, limits);
1574 }
1575 config = apply_runtime_limits(config, num_threads, lean_max_memory_kib);
1576 if let Some(cap) = max_frame_bytes {
1577 config = config.max_frame_bytes(cap);
1578 }
1579
1580 let mut worker = LeanWorker::spawn(&config)?;
1581 worker.health()?;
1582 Ok(worker)
1583}
1584
1585fn apply_runtime_limits(
1591 mut config: LeanWorkerConfig,
1592 num_threads: Option<u32>,
1593 lean_max_memory_kib: Option<u64>,
1594) -> LeanWorkerConfig {
1595 if let Some(threads) = num_threads {
1596 config = config.env("LEAN_RS_NUM_THREADS", threads.to_string());
1597 }
1598 if let Some(limit_kib) = lean_max_memory_kib {
1599 config = config.env("LEAN_RS_LEAN_MAX_MEMORY_KIB", limit_kib.to_string());
1600 }
1601 config
1602}
1603
1604fn apply_module_cache_limits(mut config: LeanWorkerConfig, limits: &LeanWorkerModuleCacheLimits) -> LeanWorkerConfig {
1605 if let Some(value) = limits.max_entries {
1606 config = config.env("LEAN_RS_MODULE_CACHE_MAX_ENTRIES", value.to_string());
1607 }
1608 if let Some(value) = limits.ttl_millis {
1609 config = config.env("LEAN_RS_MODULE_CACHE_TTL_MILLIS", value.to_string());
1610 }
1611 if let Some(value) = limits.max_bytes {
1612 config = config.env("LEAN_RS_MODULE_CACHE_MAX_BYTES", value.to_string());
1613 }
1614 if let Some(value) = limits.rss_guard_kib {
1615 config = config.env("LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB", value.to_string());
1616 }
1617 if let Some(value) = limits.verify_rss_taint_kib {
1618 config = config.env("LEAN_RS_VERIFY_RSS_TAINT_KIB", value.to_string());
1619 }
1620 config
1621}
1622
1623#[derive(Clone, Debug, Eq, PartialEq)]
1673pub struct LeanWorkerChild {
1674 executable_name: Option<String>,
1675 explicit_path: Option<PathBuf>,
1676 env_var: Option<String>,
1677 lean_sysroot: Option<PathBuf>,
1678}
1679
1680impl LeanWorkerChild {
1681 #[must_use]
1691 pub fn sibling(executable_name: impl Into<String>) -> Self {
1692 Self {
1693 executable_name: Some(with_exe_suffix(executable_name.into())),
1694 explicit_path: None,
1695 env_var: None,
1696 lean_sysroot: None,
1697 }
1698 }
1699
1700 #[must_use]
1702 pub fn path(path: impl Into<PathBuf>) -> Self {
1703 Self {
1704 executable_name: None,
1705 explicit_path: Some(path.into()),
1706 env_var: None,
1707 lean_sysroot: None,
1708 }
1709 }
1710
1711 #[must_use]
1719 pub fn for_toolchain(path: impl Into<PathBuf>, sysroot: impl Into<PathBuf>) -> Self {
1720 Self {
1721 executable_name: None,
1722 explicit_path: Some(path.into()),
1723 env_var: None,
1724 lean_sysroot: Some(sysroot.into()),
1725 }
1726 }
1727
1728 #[must_use]
1733 pub fn lean_sysroot(mut self, sysroot: impl Into<PathBuf>) -> Self {
1734 self.lean_sysroot = Some(sysroot.into());
1735 self
1736 }
1737
1738 #[must_use]
1740 pub fn env_override(mut self, env_var: impl Into<String>) -> Self {
1741 self.env_var = Some(env_var.into());
1742 self
1743 }
1744
1745 fn resolve_lean_sysroot(&self) -> Result<PathBuf, LeanWorkerError> {
1752 if let Some(sysroot) = &self.lean_sysroot {
1753 return Ok(sysroot.clone());
1754 }
1755 let info = lean_toolchain::discover_toolchain(&lean_toolchain::DiscoverOptions::default()).map_err(|diag| {
1756 LeanWorkerError::Setup {
1757 message: format!("could not discover Lean sysroot for worker spawn: {diag}"),
1758 }
1759 })?;
1760 Ok(info.prefix)
1761 }
1762
1763 fn resolve(&self) -> Result<PathBuf, LeanWorkerError> {
1764 let mut tried = Vec::new();
1765 if let Some(env_var) = &self.env_var
1766 && let Some(value) = env::var_os(env_var)
1767 {
1768 let path = PathBuf::from(value);
1769 if path.is_file() {
1770 return Ok(path);
1771 }
1772 tried.push(path);
1773 return Err(LeanWorkerError::WorkerChildUnresolved { tried });
1774 }
1775 if let Some(path) = &self.explicit_path {
1776 return Ok(path.clone());
1777 }
1778
1779 let executable_name = self
1780 .executable_name
1781 .clone()
1782 .unwrap_or_else(|| with_exe_suffix("lean-rs-worker-child".to_owned()));
1783 tried.extend(candidate_sibling_worker_paths(&executable_name));
1784 if executable_name == with_exe_suffix("lean-rs-worker-child".to_owned())
1789 && let Some(path) = try_build_workspace_worker_child(&executable_name, &mut tried)
1790 {
1791 return Ok(path);
1792 }
1793 for path in dedup_paths(&tried) {
1794 if path.is_file() {
1795 return Ok(path);
1796 }
1797 }
1798 Err(LeanWorkerError::WorkerChildUnresolved { tried })
1799 }
1800}
1801
1802impl Default for LeanWorkerChild {
1803 fn default() -> Self {
1804 Self::sibling("lean-rs-worker-child").env_override(WORKER_CHILD_ENV)
1805 }
1806}
1807
1808fn resolve_default_worker_executable() -> Result<PathBuf, LeanWorkerError> {
1809 LeanWorkerChild::default().resolve()
1810}
1811
1812fn validate_worker_child_path(path: &Path) -> Result<(), LeanWorkerError> {
1813 if !path.is_file() {
1814 return Err(LeanWorkerError::WorkerChildNotExecutable {
1815 path: path.to_path_buf(),
1816 reason: "path does not point to a file".to_owned(),
1817 });
1818 }
1819 if !is_executable_file(path) {
1820 return Err(LeanWorkerError::WorkerChildNotExecutable {
1821 path: path.to_path_buf(),
1822 reason: "file is not executable by this user".to_owned(),
1823 });
1824 }
1825 Ok(())
1826}
1827
1828#[cfg(unix)]
1829fn is_executable_file(path: &Path) -> bool {
1830 use std::os::unix::fs::PermissionsExt as _;
1831
1832 std::fs::metadata(path).is_ok_and(|metadata| metadata.permissions().mode() & 0o111 != 0)
1833}
1834
1835#[cfg(not(unix))]
1836fn is_executable_file(_path: &Path) -> bool {
1837 true
1838}
1839
1840fn check_from_open_error(err: &LeanWorkerError) -> LeanWorkerBootstrapCheck {
1841 match err {
1842 LeanWorkerError::WorkerChildUnresolved { tried } => LeanWorkerBootstrapCheck::error(
1843 LeanWorkerBootstrapDiagnosticCode::WorkerChildUnresolved,
1844 "worker child",
1845 format!("could not resolve worker child; tried {}", format_paths(tried)),
1846 "ship an app-owned worker child binary beside the app or configure LeanWorkerChild::env_override",
1847 ),
1848 LeanWorkerError::WorkerChildNotExecutable { path, reason } => LeanWorkerBootstrapCheck::error(
1849 LeanWorkerBootstrapDiagnosticCode::WorkerChildNotExecutable,
1850 path.display().to_string(),
1851 reason.clone(),
1852 "ship an app-owned worker child binary and ensure it is executable",
1853 ),
1854 LeanWorkerError::Bootstrap { code, message } => LeanWorkerBootstrapCheck::error(
1855 *code,
1856 code.as_str(),
1857 message.clone(),
1858 "fix the reported bootstrap input",
1859 ),
1860 LeanWorkerError::Handshake { message } => LeanWorkerBootstrapCheck::error(
1861 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1862 "worker handshake",
1863 message.clone(),
1864 "ensure the worker child calls lean_rs_worker_child::run_worker_child_stdio and matches this crate version",
1865 ),
1866 LeanWorkerError::Timeout {
1867 operation: "startup", ..
1868 } => LeanWorkerBootstrapCheck::error(
1869 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1870 "worker handshake",
1871 err.to_string(),
1872 "check that the worker child starts promptly and writes the lean-rs-worker handshake",
1873 ),
1874 LeanWorkerError::CapabilityMetadataMismatch { export, .. } => LeanWorkerBootstrapCheck::error(
1875 LeanWorkerBootstrapDiagnosticCode::CapabilityMetadataMismatch,
1876 export.clone(),
1877 "capability metadata did not match the requested expectation",
1878 "rebuild or select a capability whose metadata matches the caller expectation",
1879 ),
1880 other @ (LeanWorkerError::Spawn { .. }
1881 | LeanWorkerError::CapabilityBuild { .. }
1882 | LeanWorkerError::Setup { .. }
1883 | LeanWorkerError::Protocol { .. }
1884 | LeanWorkerError::Worker { .. }
1885 | LeanWorkerError::ChildExited { .. }
1886 | LeanWorkerError::ChildPanicOrAbort { .. }
1887 | LeanWorkerError::Timeout { .. }
1888 | LeanWorkerError::RssHardLimitExceeded { .. }
1889 | LeanWorkerError::Cancelled { .. }
1890 | LeanWorkerError::ProgressPanic { .. }
1891 | LeanWorkerError::DataSinkPanic { .. }
1892 | LeanWorkerError::DiagnosticSinkPanic { .. }
1893 | LeanWorkerError::StreamExportFailed { .. }
1894 | LeanWorkerError::StreamCallbackFailed { .. }
1895 | LeanWorkerError::StreamRowMalformed { .. }
1896 | LeanWorkerError::CapabilityMetadataMalformed { .. }
1897 | LeanWorkerError::CapabilityDoctorMalformed { .. }
1898 | LeanWorkerError::TypedCommandRequestEncode { .. }
1899 | LeanWorkerError::TypedCommandResponseDecode { .. }
1900 | LeanWorkerError::TypedCommandRowDecode { .. }
1901 | LeanWorkerError::TypedCommandSummaryDecode { .. }
1902 | LeanWorkerError::LeaseInvalidated { .. }
1903 | LeanWorkerError::WorkerPoolExhausted { .. }
1904 | LeanWorkerError::WorkerPoolMemoryBudgetExceeded { .. }
1905 | LeanWorkerError::WorkerPoolQueueTimeout { .. }
1906 | LeanWorkerError::RestartLimitExceeded { .. }
1907 | LeanWorkerError::UnsupportedRequest { .. }
1908 | LeanWorkerError::Wait { .. }
1909 | LeanWorkerError::Kill { .. }
1910 | LeanWorkerError::WaitTimeout { .. }
1911 | LeanWorkerError::ShutdownInProgress { .. }) => LeanWorkerBootstrapCheck::error(
1912 LeanWorkerBootstrapDiagnosticCode::WorkerStartupFailed,
1913 "worker bootstrap",
1914 other.to_string(),
1915 "run the bootstrap check in a deployment environment and rebuild the worker child or capability artifact",
1916 ),
1917 }
1918}
1919
1920fn format_paths(paths: &[PathBuf]) -> String {
1921 if paths.is_empty() {
1922 return "<none>".to_owned();
1923 }
1924 paths
1925 .iter()
1926 .map(|path| path.display().to_string())
1927 .collect::<Vec<_>>()
1928 .join(", ")
1929}
1930
1931fn bound_bootstrap_text(mut text: String) -> String {
1932 const LIMIT: usize = 1_024;
1933 if text.len() <= LIMIT {
1934 return text;
1935 }
1936 while !text.is_char_boundary(LIMIT) {
1937 text.pop();
1938 }
1939 text.truncate(LIMIT);
1940 text.push_str("...");
1941 text
1942}
1943
1944fn candidate_sibling_worker_paths(executable_name: &str) -> Vec<PathBuf> {
1945 let mut tried = Vec::new();
1946 if let Ok(current_exe) = env::current_exe() {
1947 if let Some(dir) = current_exe.parent() {
1948 tried.push(dir.join(executable_name));
1949 }
1950 if let Some(profile_dir) = current_exe.parent().and_then(Path::parent) {
1951 tried.push(profile_dir.join(executable_name));
1952 }
1953 }
1954 tried
1955}
1956
1957fn with_exe_suffix(mut executable_name: String) -> String {
1958 if !env::consts::EXE_SUFFIX.is_empty() && !executable_name.ends_with(env::consts::EXE_SUFFIX) {
1959 executable_name.push_str(env::consts::EXE_SUFFIX);
1960 }
1961 executable_name
1962}
1963
1964fn infer_lake_project_root_from_dylib(dylib_path: &Path) -> Result<PathBuf, LeanWorkerError> {
1965 let lib_dir = dylib_path.parent();
1966 let build_dir = lib_dir.and_then(Path::parent);
1967 let lake_dir = build_dir.and_then(Path::parent);
1968 let project_root = lake_dir.and_then(Path::parent);
1969 match (lib_dir, build_dir, lake_dir, project_root) {
1970 (Some(lib), Some(build), Some(lake), Some(root))
1971 if lib.file_name().is_some_and(|name| name == "lib")
1972 && build.file_name().is_some_and(|name| name == "build")
1973 && lake.file_name().is_some_and(|name| name == ".lake") =>
1974 {
1975 Ok(root.to_path_buf())
1976 }
1977 _ => Err(LeanWorkerError::Setup {
1978 message: format!(
1979 "built capability dylib '{}' is not under a standard .lake/build/lib directory",
1980 dylib_path.display()
1981 ),
1982 }),
1983 }
1984}
1985
1986fn normalize_import_workspace_root(path: PathBuf) -> PathBuf {
1987 std::fs::canonicalize(&path).unwrap_or(path)
1988}
1989
1990fn try_build_workspace_worker_child(executable_name: &str, tried: &mut Vec<PathBuf>) -> Option<PathBuf> {
2001 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2002 let workspace = manifest_dir.parent()?.parent()?;
2003 if !workspace
2004 .join("crates")
2005 .join("lean-rs-worker-child")
2006 .join("Cargo.toml")
2007 .is_file()
2008 {
2009 return None;
2010 }
2011
2012 let debug = workspace.join("target").join("debug").join(executable_name);
2013 let release = workspace.join("target").join("release").join(executable_name);
2014 tried.push(debug.clone());
2015 tried.push(release.clone());
2016 if debug.is_file() {
2017 return Some(debug);
2018 }
2019 if release.is_file() {
2020 return Some(release);
2021 }
2022
2023 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
2024 let status = Command::new(cargo)
2025 .current_dir(workspace)
2026 .args(["build", "-p", "lean-rs-worker-child", "--bin", "lean-rs-worker-child"])
2027 .status()
2028 .ok()?;
2029 if !status.success() {
2030 return None;
2031 }
2032 debug.is_file().then_some(debug)
2033}
2034
2035fn dedup_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
2036 let mut unique = Vec::new();
2037 for path in paths {
2038 if !unique.iter().any(|existing| existing == path) {
2039 unique.push(path.clone());
2040 }
2041 }
2042 unique
2043}
2044
2045#[cfg(test)]
2046#[allow(clippy::expect_used, clippy::panic)]
2047mod tests {
2048 use super::{
2049 LeanWorkerCapabilityBuilder, LeanWorkerChild, LeanWorkerModuleCacheLimits, apply_module_cache_limits,
2050 apply_runtime_limits,
2051 };
2052 use crate::supervisor::LeanWorkerConfig;
2053 use lean_rs_worker_protocol::types::LeanWorkerSessionImportProfile;
2054 use lean_toolchain::LeanBuiltCapability;
2055 use std::path::PathBuf;
2056
2057 fn workspace_root() -> PathBuf {
2058 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2059 manifest_dir
2060 .parent()
2061 .and_then(std::path::Path::parent)
2062 .expect("crates/<name> lives two directories below the workspace root")
2063 .to_path_buf()
2064 }
2065
2066 fn interop_root() -> PathBuf {
2067 workspace_root().join("fixtures").join("interop-shims")
2068 }
2069
2070 fn capability_builder() -> LeanWorkerCapabilityBuilder {
2071 LeanWorkerCapabilityBuilder::new(
2072 interop_root(),
2073 "lean_rs_interop_consumer",
2074 "LeanRsInteropConsumer",
2075 ["LeanRsInteropConsumer.Callback"],
2076 )
2077 }
2078
2079 #[test]
2080 fn import_workspace_root_unset_matches_capability_project_root() {
2081 let key = capability_builder().session_key();
2082 assert_eq!(key.project_root(), interop_root().as_path());
2083 assert_eq!(key.import_workspace_root(), interop_root().as_path());
2084 }
2085
2086 #[test]
2087 fn project_root_is_canonicalized_for_session_reuse() {
2088 assert_eq!(
2089 capability_builder().session_key(),
2090 LeanWorkerCapabilityBuilder::new(
2091 interop_root().join("."),
2092 "lean_rs_interop_consumer",
2093 "LeanRsInteropConsumer",
2094 ["LeanRsInteropConsumer.Callback"],
2095 )
2096 .session_key(),
2097 );
2098 }
2099
2100 #[test]
2101 fn explicit_import_workspace_root_matching_capability_root_preserves_key() {
2102 assert_eq!(
2103 capability_builder().session_key(),
2104 capability_builder().import_workspace_root(interop_root()).session_key()
2105 );
2106 }
2107
2108 #[test]
2109 fn import_workspace_root_is_canonicalized_for_session_reuse() {
2110 assert_eq!(
2111 capability_builder().import_workspace_root(interop_root()).session_key(),
2112 capability_builder()
2113 .import_workspace_root(interop_root().join("."))
2114 .session_key(),
2115 );
2116 }
2117
2118 #[test]
2119 fn import_workspace_root_participates_in_session_key() {
2120 assert_ne!(
2121 capability_builder().session_key(),
2122 capability_builder()
2123 .import_workspace_root(workspace_root())
2124 .session_key(),
2125 );
2126 }
2127
2128 #[test]
2129 fn import_profile_participates_in_session_key() {
2130 assert_ne!(
2131 capability_builder().session_key(),
2132 capability_builder()
2133 .import_profile(LeanWorkerSessionImportProfile::FullPrivateCompat)
2134 .session_key(),
2135 );
2136 }
2137
2138 #[test]
2139 fn built_manifest_path_participates_in_session_key() {
2140 let manifest_dir = std::env::temp_dir().join(format!("lean-rs-worker-manifest-key-{}", std::process::id()));
2141 std::fs::create_dir_all(&manifest_dir).expect("manifest temp dir");
2142 let dylib = interop_root()
2143 .join(".lake")
2144 .join("build")
2145 .join("lib")
2146 .join(if cfg!(target_os = "macos") {
2147 "liblean__rs__interop__consumer_LeanRsInteropConsumer.dylib"
2148 } else {
2149 "liblean__rs__interop__consumer_LeanRsInteropConsumer.so"
2150 });
2151 let manifest_a = manifest_dir.join("a.json");
2152 let manifest_b = manifest_dir.join("b.json");
2153 for manifest in [&manifest_a, &manifest_b] {
2154 std::fs::write(
2155 manifest,
2156 format!(
2157 r#"{{"schema_version":2,"primary_dylib":{},"package":"lean_rs_interop_consumer","module":"LeanRsInteropConsumer"}}"#,
2158 serde_json::to_string(&dylib).expect("dylib path json")
2159 ),
2160 )
2161 .expect("write manifest");
2162 }
2163
2164 let key_a = LeanWorkerCapabilityBuilder::from_built_capability(
2165 &LeanBuiltCapability::manifest_path(&manifest_a),
2166 ["LeanRsInteropConsumer.Callback"],
2167 )
2168 .expect("manifest A accepted")
2169 .session_key();
2170 let key_a_dot = LeanWorkerCapabilityBuilder::from_built_capability(
2171 &LeanBuiltCapability::manifest_path(manifest_dir.join(".").join("a.json")),
2172 ["LeanRsInteropConsumer.Callback"],
2173 )
2174 .expect("canonical-equivalent manifest accepted")
2175 .session_key();
2176 let key_b = LeanWorkerCapabilityBuilder::from_built_capability(
2177 &LeanBuiltCapability::manifest_path(&manifest_b),
2178 ["LeanRsInteropConsumer.Callback"],
2179 )
2180 .expect("manifest B accepted")
2181 .session_key();
2182
2183 assert_eq!(key_a, key_a_dot);
2184 assert_ne!(key_a, key_b);
2185 drop(std::fs::remove_dir_all(manifest_dir));
2186 }
2187
2188 #[test]
2189 fn for_toolchain_carries_sysroot_through_resolve() {
2190 let sysroot = PathBuf::from("/opt/some/lean/prefix");
2191 let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2192 let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2193 assert_eq!(resolved, sysroot);
2194 }
2195
2196 #[test]
2197 fn lean_sysroot_setter_overrides_default() {
2198 let sysroot = PathBuf::from("/opt/override/lean");
2199 let child = LeanWorkerChild::path("/opt/worker").lean_sysroot(&sysroot);
2200 let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2201 assert_eq!(resolved, sysroot);
2202 }
2203
2204 #[test]
2205 fn explicit_sysroot_bypasses_discovery_even_when_path_is_nonexistent() {
2206 let sysroot = PathBuf::from("/definitely/not/a/real/sysroot");
2211 let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2212 let resolved = child
2213 .resolve_lean_sysroot()
2214 .expect("explicit sysroot resolves without filesystem checks");
2215 assert_eq!(resolved, sysroot);
2216 }
2217
2218 #[test]
2219 fn module_cache_limits_map_to_typed_child_policy_env() {
2220 let limits = LeanWorkerModuleCacheLimits::default()
2221 .max_entries(7)
2222 .ttl(std::time::Duration::from_millis(250))
2223 .max_bytes(4096)
2224 .rss_guard_kib(8192);
2225 let config = apply_module_cache_limits(LeanWorkerConfig::new("/opt/worker"), &limits);
2226 let env = config.env_overrides();
2227 assert!(
2228 env.iter()
2229 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_ENTRIES" && v == "7")
2230 );
2231 assert!(
2232 env.iter()
2233 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_TTL_MILLIS" && v == "250")
2234 );
2235 assert!(
2236 env.iter()
2237 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_BYTES" && v == "4096")
2238 );
2239 assert!(
2240 env.iter()
2241 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB" && v == "8192")
2242 );
2243 }
2244
2245 #[test]
2246 fn runtime_limits_map_to_typed_child_env() {
2247 let config = apply_runtime_limits(LeanWorkerConfig::new("/opt/worker"), Some(1), Some(16_777_216));
2248 let env = config.env_overrides();
2249 assert!(env.iter().any(|(k, v)| k == "LEAN_RS_NUM_THREADS" && v == "1"));
2250 assert!(
2251 env.iter()
2252 .any(|(k, v)| k == "LEAN_RS_LEAN_MAX_MEMORY_KIB" && v == "16777216")
2253 );
2254 }
2255
2256 #[test]
2257 fn runtime_limits_are_absent_when_unset() {
2258 let config = apply_runtime_limits(LeanWorkerConfig::new("/opt/worker"), None, None);
2259 let env = config.env_overrides();
2260 assert!(!env.iter().any(|(k, _)| k == "LEAN_RS_NUM_THREADS"));
2261 assert!(!env.iter().any(|(k, _)| k == "LEAN_RS_LEAN_MAX_MEMORY_KIB"));
2262 }
2263}