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 metadata_check: Option<CapabilityMetadataCheck>,
80 max_frame_bytes: Option<u32>,
81 worker_export_signatures: Vec<LeanExportSignature>,
82}
83
84impl LeanWorkerCapabilityBuilder {
85 #[must_use]
94 pub fn new(
95 project_root: impl Into<PathBuf>,
96 package: impl Into<String>,
97 lib_name: impl Into<String>,
98 imports: impl IntoIterator<Item = impl Into<String>>,
99 ) -> Self {
100 Self {
101 project_root: project_root.into(),
102 import_workspace_root: None,
103 package: package.into(),
104 lib_name: lib_name.into(),
105 imports: imports.into_iter().map(Into::into).collect(),
106 import_profile: LeanWorkerSessionImportProfile::default(),
107 built_dylib_path: None,
108 built_manifest_path: None,
109 built_capability: None,
110 worker_child: None,
111 startup_timeout: None,
112 request_timeout: None,
113 shutdown_timeout: None,
114 restart_policy: None,
115 rss_hard_limit: None,
116 module_cache_limits: None,
117 metadata_check: None,
118 max_frame_bytes: None,
119 worker_export_signatures: Vec::new(),
120 }
121 }
122
123 pub fn from_built_capability(
140 spec: &LeanBuiltCapability,
141 imports: impl IntoIterator<Item = impl Into<String>>,
142 ) -> Result<Self, LeanWorkerError> {
143 let artifact = WorkerCapabilityArtifact::from_built_capability(spec)?;
144 let project_root = infer_lake_project_root_from_dylib(&artifact.dylib_path)?;
145 Ok(Self {
146 project_root,
147 import_workspace_root: None,
148 package: artifact.package,
149 lib_name: artifact.module,
150 imports: imports.into_iter().map(Into::into).collect(),
151 import_profile: LeanWorkerSessionImportProfile::default(),
152 built_dylib_path: Some(artifact.dylib_path),
153 built_manifest_path: artifact.manifest_path,
154 built_capability: Some(spec.clone()),
155 worker_child: None,
156 startup_timeout: None,
157 request_timeout: None,
158 shutdown_timeout: None,
159 restart_policy: None,
160 rss_hard_limit: None,
161 module_cache_limits: None,
162 metadata_check: None,
163 max_frame_bytes: None,
164 worker_export_signatures: Vec::new(),
165 })
166 }
167
168 #[must_use]
173 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
174 self.worker_child = Some(LeanWorkerChild::path(path));
175 self
176 }
177
178 #[must_use]
180 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
181 self.worker_child = Some(child);
182 self
183 }
184
185 #[must_use]
202 pub fn import_workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
203 self.import_workspace_root = Some(normalize_import_workspace_root(path.into()));
204 self
205 }
206
207 #[must_use]
209 pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
210 self.import_profile = profile;
211 self
212 }
213
214 #[must_use]
216 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
217 self.startup_timeout = Some(timeout);
218 self
219 }
220
221 #[must_use]
223 pub fn request_timeout(mut self, timeout: Duration) -> Self {
224 self.request_timeout = Some(timeout);
225 self
226 }
227
228 #[must_use]
230 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
231 self.shutdown_timeout = Some(timeout);
232 self
233 }
234
235 #[must_use]
237 pub fn long_running_requests(mut self) -> Self {
238 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
239 self
240 }
241
242 #[must_use]
244 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
245 self.restart_policy = Some(policy);
246 self
247 }
248
249 #[must_use]
252 pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
253 self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
254 self
255 }
256
257 #[must_use]
263 pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
264 self.module_cache_limits = Some(limits);
265 self
266 }
267
268 #[must_use]
276 pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
277 self.max_frame_bytes = Some(max_frame_bytes);
278 self
279 }
280
281 #[must_use]
287 pub fn validate_metadata(mut self, export: impl Into<String>, request: Value) -> Self {
288 let export = export.into();
289 self.add_worker_export_signature(metadata_signature(export.clone()));
290 self.metadata_check = Some(CapabilityMetadataCheck {
291 export,
292 request,
293 expected: None,
294 });
295 self
296 }
297
298 #[must_use]
304 pub fn expect_metadata(
305 mut self,
306 export: impl Into<String>,
307 request: Value,
308 expected: LeanWorkerCapabilityMetadata,
309 ) -> Self {
310 let export = export.into();
311 self.add_worker_export_signature(metadata_signature(export.clone()));
312 self.metadata_check = Some(CapabilityMetadataCheck {
313 export,
314 request,
315 expected: Some(expected),
316 });
317 self
318 }
319
320 #[must_use]
322 pub fn metadata_export(mut self, export: impl Into<String>) -> Self {
323 self.add_worker_export_signature(metadata_signature(export));
324 self
325 }
326
327 #[must_use]
329 pub fn doctor_export(mut self, export: impl Into<String>) -> Self {
330 self.add_worker_export_signature(doctor_signature(export));
331 self
332 }
333
334 #[must_use]
336 pub fn json_command_export(mut self, export: impl Into<String>) -> Self {
337 self.add_worker_export_signature(json_command_signature(export));
338 self
339 }
340
341 #[must_use]
343 pub fn streaming_command_export(mut self, export: impl Into<String>) -> Self {
344 self.add_worker_export_signature(streaming_command_signature(export));
345 self
346 }
347
348 fn add_worker_export_signature(&mut self, signature: LeanExportSignature) {
349 if self
350 .worker_export_signatures
351 .iter()
352 .all(|existing| existing.symbol() != signature.symbol())
353 {
354 self.worker_export_signatures.push(signature);
355 }
356 }
357
358 #[must_use]
364 pub fn session_key(&self) -> LeanWorkerSessionKey {
365 let restart_policy_class = match &self.restart_policy {
366 Some(policy) if policy == &LeanWorkerRestartPolicy::default() => LeanWorkerRestartPolicyClass::Default,
367 Some(_policy) => LeanWorkerRestartPolicyClass::Custom,
368 None => LeanWorkerRestartPolicyClass::Default,
369 };
370 let mut key = LeanWorkerSessionKey::new(
371 self.project_root.clone(),
372 self.package.clone(),
373 self.lib_name.clone(),
374 self.imports.clone(),
375 )
376 .with_import_profile(self.import_profile)
377 .with_import_workspace_root(self.effective_import_workspace_root())
378 .restart_policy_class(restart_policy_class);
379 if let Some(manifest_path) = &self.built_manifest_path {
380 key = key.with_built_manifest_path(manifest_path.clone());
381 }
382 if let Some(check) = &self.metadata_check {
383 key = key.metadata_expectation(check.export.clone(), check.request.clone(), check.expected.clone());
384 }
385 key
386 }
387
388 fn effective_import_workspace_root(&self) -> PathBuf {
389 self.import_workspace_root
390 .clone()
391 .unwrap_or_else(|| normalize_import_workspace_root(self.project_root.clone()))
392 }
393
394 pub(crate) fn pool_request_timeout(&self) -> Duration {
395 self.request_timeout
396 .unwrap_or(crate::supervisor::LEAN_WORKER_REQUEST_TIMEOUT_DEFAULT)
397 }
398
399 #[must_use]
407 pub fn check(&self) -> LeanWorkerBootstrapReport {
408 let mut checks = self.bootstrap_static_checks();
409 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
410 return LeanWorkerBootstrapReport::new(checks);
411 }
412
413 match self.clone().open_unchecked() {
414 Ok(capability) => {
415 drop(capability.shutdown());
416 }
417 Err(err) => checks.push(check_from_open_error(&err)),
418 }
419 LeanWorkerBootstrapReport::new(checks)
420 }
421
422 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
423 let mut checks = Vec::new();
424 checks.extend(worker_child_static_checks(self.worker_child.as_ref()));
425
426 if let Some(spec) = &self.built_capability
427 && let Ok(manifest_path) = spec.resolved_manifest_path()
428 {
429 let report = lean_toolchain::manifest_validation::check_static(&manifest_path);
430 for check in report.errors() {
431 checks.push(LeanWorkerBootstrapCheck::error(
432 LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight { code: check.code() },
433 check.subject().to_owned(),
434 check.message().to_owned(),
435 check.repair_hint().to_owned(),
436 ));
437 }
438 }
439 checks
440 }
441
442 pub fn open(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
450 let report = self.bootstrap_static_report();
451 if let Some(check) = report.first_error() {
452 return Err(LeanWorkerError::Bootstrap {
453 code: check.code(),
454 message: check.message().to_owned(),
455 });
456 }
457 self.open_unchecked()
458 }
459
460 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
461 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
462 }
463
464 fn open_unchecked(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
465 let import_workspace_root = self.effective_import_workspace_root();
466 let capability_load_started = Instant::now();
467 let (dylib_path, manifest_path) = match (self.built_dylib_path, self.built_manifest_path) {
468 (Some(dylib_path), Some(manifest_path)) => (dylib_path, manifest_path),
469 (_, None) => {
470 let mut builder = lean_toolchain::CargoLeanCapability::new(&self.project_root, &self.lib_name)
471 .package(&self.package)
472 .module(&self.lib_name);
473 for signature in self.worker_export_signatures {
474 builder = builder.export_signature(signature);
475 }
476 let built = builder
477 .build_quiet()
478 .map_err(|diagnostic| LeanWorkerError::CapabilityBuild { diagnostic })?;
479 (built.dylib_path().to_path_buf(), built.manifest_path().to_path_buf())
480 }
481 (None, Some(manifest_path)) => {
482 let artifact = WorkerCapabilityArtifact::from_manifest(&manifest_path)?;
483 (artifact.dylib_path, manifest_path)
484 }
485 };
486 let capability_load_elapsed = capability_load_started.elapsed();
487 let mut worker = spawn_checked_worker(
488 self.worker_child,
489 self.startup_timeout,
490 self.request_timeout,
491 self.shutdown_timeout,
492 self.restart_policy,
493 self.rss_hard_limit,
494 self.module_cache_limits,
495 self.max_frame_bytes,
496 )?;
497
498 let session_config = LeanWorkerSessionConfig::manifest_backed(
499 import_workspace_root,
500 self.package.clone(),
501 self.lib_name.clone(),
502 manifest_path,
503 self.imports.clone(),
504 )
505 .with_import_profile(self.import_profile);
506
507 let session_open_import_elapsed;
508 let validated_metadata = {
509 let session_open_started = Instant::now();
510 let mut session = worker.open_session(&session_config, None, None)?;
511 session_open_import_elapsed = session_open_started.elapsed();
512 match self.metadata_check {
513 Some(check) => {
514 let metadata = session.capability_metadata(&check.export, &check.request, None, None)?;
515 if let Some(expected) = check.expected
516 && metadata != expected
517 {
518 return Err(LeanWorkerError::CapabilityMetadataMismatch {
519 export: check.export,
520 expected: Box::new(expected),
521 actual: Box::new(metadata),
522 });
523 }
524 Some(metadata)
525 }
526 None => None,
527 }
528 };
529 worker.record_capability_open_timing(capability_load_elapsed, session_open_import_elapsed);
530
531 Ok(LeanWorkerCapability {
532 worker,
533 session_config,
534 dylib_path,
535 validated_metadata,
536 })
537 }
538}
539
540#[derive(Clone, Debug)]
548pub struct LeanWorkerHostHandleBuilder {
549 project_root: PathBuf,
550 imports: Vec<String>,
551 import_profile: LeanWorkerSessionImportProfile,
552 worker_child: Option<LeanWorkerChild>,
553 startup_timeout: Option<Duration>,
554 request_timeout: Option<Duration>,
555 shutdown_timeout: Option<Duration>,
556 restart_policy: Option<LeanWorkerRestartPolicy>,
557 rss_hard_limit: Option<(u64, Duration)>,
558 module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
559 max_frame_bytes: Option<u32>,
560}
561
562#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
568pub struct LeanWorkerModuleCacheLimits {
569 max_entries: Option<u64>,
570 ttl_millis: Option<u64>,
571 max_bytes: Option<u64>,
572 rss_guard_kib: Option<u64>,
573 verify_rss_taint_kib: Option<u64>,
574}
575
576impl LeanWorkerModuleCacheLimits {
577 #[must_use]
579 pub fn max_entries(mut self, max_entries: u64) -> Self {
580 self.max_entries = Some(max_entries.max(1));
581 self
582 }
583
584 #[must_use]
586 pub fn ttl(mut self, ttl: Duration) -> Self {
587 self.ttl_millis = Some(u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX).max(1));
588 self
589 }
590
591 #[must_use]
593 pub fn max_bytes(mut self, max_bytes: u64) -> Self {
594 self.max_bytes = Some(max_bytes.max(1));
595 self
596 }
597
598 #[must_use]
601 pub fn rss_guard_kib(mut self, rss_guard_kib: u64) -> Self {
602 self.rss_guard_kib = Some(rss_guard_kib.max(1));
603 self
604 }
605
606 #[must_use]
613 pub fn verify_rss_taint_kib(mut self, verify_rss_taint_kib: u64) -> Self {
614 self.verify_rss_taint_kib = Some(verify_rss_taint_kib.max(1));
615 self
616 }
617}
618
619impl LeanWorkerHostHandleBuilder {
620 #[must_use]
626 pub fn shims_only(project_root: impl Into<PathBuf>, imports: impl IntoIterator<Item = impl Into<String>>) -> Self {
627 Self {
628 project_root: project_root.into(),
629 imports: imports.into_iter().map(Into::into).collect(),
630 import_profile: LeanWorkerSessionImportProfile::default(),
631 worker_child: None,
632 startup_timeout: None,
633 request_timeout: None,
634 shutdown_timeout: None,
635 restart_policy: None,
636 rss_hard_limit: None,
637 module_cache_limits: None,
638 max_frame_bytes: None,
639 }
640 }
641
642 #[must_use]
644 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
645 self.worker_child = Some(LeanWorkerChild::path(path));
646 self
647 }
648
649 #[must_use]
651 pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
652 self.import_profile = profile;
653 self
654 }
655
656 #[must_use]
658 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
659 self.worker_child = Some(child);
660 self
661 }
662
663 #[must_use]
665 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
666 self.startup_timeout = Some(timeout);
667 self
668 }
669
670 #[must_use]
672 pub fn request_timeout(mut self, timeout: Duration) -> Self {
673 self.request_timeout = Some(timeout);
674 self
675 }
676
677 #[must_use]
679 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
680 self.shutdown_timeout = Some(timeout);
681 self
682 }
683
684 #[must_use]
686 pub fn long_running_requests(mut self) -> Self {
687 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
688 self
689 }
690
691 #[must_use]
693 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
694 self.restart_policy = Some(policy);
695 self
696 }
697
698 #[must_use]
701 pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
702 self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
703 self
704 }
705
706 #[must_use]
711 pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
712 self.module_cache_limits = Some(limits);
713 self
714 }
715
716 #[must_use]
718 pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
719 self.max_frame_bytes = Some(max_frame_bytes);
720 self
721 }
722
723 #[must_use]
728 pub fn check(&self) -> LeanWorkerBootstrapReport {
729 let mut checks = self.bootstrap_static_checks();
730 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
731 return LeanWorkerBootstrapReport::new(checks);
732 }
733
734 match self.clone().open_unchecked() {
735 Ok(handle) => {
736 drop(handle.shutdown());
737 }
738 Err(err) => checks.push(check_from_open_error(&err)),
739 }
740 LeanWorkerBootstrapReport::new(checks)
741 }
742
743 pub fn open(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
751 let report = self.bootstrap_static_report();
752 if let Some(check) = report.first_error() {
753 return Err(LeanWorkerError::Bootstrap {
754 code: check.code(),
755 message: check.message().to_owned(),
756 });
757 }
758 self.open_unchecked()
759 }
760
761 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
762 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
763 }
764
765 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
766 worker_child_static_checks(self.worker_child.as_ref())
767 }
768
769 fn open_unchecked(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
770 let mut worker = spawn_checked_worker(
771 self.worker_child,
772 self.startup_timeout,
773 self.request_timeout,
774 self.shutdown_timeout,
775 self.restart_policy,
776 self.rss_hard_limit,
777 self.module_cache_limits,
778 self.max_frame_bytes,
779 )?;
780 let session_config = LeanWorkerSessionConfig::shims_only(self.project_root, self.imports)
781 .with_import_profile(self.import_profile);
782 {
783 let _session = worker.open_session(&session_config, None, None)?;
784 }
785 Ok(LeanWorkerHostHandle { worker, session_config })
786 }
787}
788
789#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
791pub enum LeanWorkerBootstrapDiagnosticCode {
792 WorkerChildUnresolved,
794 WorkerChildNotExecutable,
796 CapabilityPreflight { code: LeanLoaderDiagnosticCode },
798 WorkerHandshakeFailed,
800 CapabilityMetadataMismatch,
802 WorkerStartupFailed,
804}
805
806impl LeanWorkerBootstrapDiagnosticCode {
807 #[must_use]
809 pub const fn as_str(self) -> &'static str {
810 match self {
811 Self::WorkerChildUnresolved => "lean_rs.worker.bootstrap.child_unresolved",
812 Self::WorkerChildNotExecutable => "lean_rs.worker.bootstrap.child_not_executable",
813 Self::CapabilityPreflight { code } => code.as_str(),
814 Self::WorkerHandshakeFailed => "lean_rs.worker.bootstrap.handshake_failed",
815 Self::CapabilityMetadataMismatch => "lean_rs.worker.bootstrap.metadata_mismatch",
816 Self::WorkerStartupFailed => "lean_rs.worker.bootstrap.startup_failed",
817 }
818 }
819}
820
821impl std::fmt::Display for LeanWorkerBootstrapDiagnosticCode {
822 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823 f.write_str(self.as_str())
824 }
825}
826
827#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
829pub enum LeanWorkerBootstrapSeverity {
830 Info,
832 Warning,
834 Error,
836}
837
838#[derive(Clone, Debug, Eq, PartialEq)]
840pub struct LeanWorkerBootstrapCheck {
841 code: LeanWorkerBootstrapDiagnosticCode,
842 severity: LeanWorkerBootstrapSeverity,
843 subject: String,
844 message: String,
845 repair_hint: String,
846}
847
848impl LeanWorkerBootstrapCheck {
849 fn error(
850 code: LeanWorkerBootstrapDiagnosticCode,
851 subject: impl Into<String>,
852 message: impl Into<String>,
853 repair_hint: impl Into<String>,
854 ) -> Self {
855 Self {
856 code,
857 severity: LeanWorkerBootstrapSeverity::Error,
858 subject: bound_bootstrap_text(subject.into()),
859 message: bound_bootstrap_text(message.into()),
860 repair_hint: bound_bootstrap_text(repair_hint.into()),
861 }
862 }
863
864 #[must_use]
866 pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
867 self.code
868 }
869
870 #[must_use]
872 pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
873 self.severity
874 }
875
876 #[must_use]
878 pub fn subject(&self) -> &str {
879 &self.subject
880 }
881
882 #[must_use]
884 pub fn message(&self) -> &str {
885 &self.message
886 }
887
888 #[must_use]
890 pub fn repair_hint(&self) -> &str {
891 &self.repair_hint
892 }
893
894 fn is_error(&self) -> bool {
895 self.severity == LeanWorkerBootstrapSeverity::Error
896 }
897}
898
899#[derive(Clone, Debug, Eq, PartialEq)]
901pub struct LeanWorkerBootstrapReport {
902 checks: Vec<LeanWorkerBootstrapCheck>,
903}
904
905impl LeanWorkerBootstrapReport {
906 fn new(checks: Vec<LeanWorkerBootstrapCheck>) -> Self {
907 Self { checks }
908 }
909
910 #[must_use]
912 pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
913 &self.checks
914 }
915
916 pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
918 self.checks
919 .iter()
920 .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
921 }
922
923 #[must_use]
925 pub fn is_ok(&self) -> bool {
926 self.first_error().is_none()
927 }
928
929 #[must_use]
931 pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
932 self.errors().next()
933 }
934}
935
936#[derive(Debug)]
942pub struct LeanWorkerCapability {
943 worker: LeanWorker,
944 session_config: LeanWorkerSessionConfig,
945 dylib_path: PathBuf,
946 validated_metadata: Option<LeanWorkerCapabilityMetadata>,
947}
948
949impl LeanWorkerCapability {
950 pub fn open_session(
962 &mut self,
963 cancellation: Option<&LeanWorkerCancellationToken>,
964 progress: Option<&dyn LeanWorkerProgressSink>,
965 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
966 self.worker.open_session(&self.session_config, cancellation, progress)
967 }
968
969 pub(crate) fn attach_open_session(&mut self) -> LeanWorkerSession<'_> {
970 self.worker.attach_open_session()
971 }
972
973 pub fn open_session_with_imports(
984 &mut self,
985 imports: impl IntoIterator<Item = impl Into<String>>,
986 cancellation: Option<&LeanWorkerCancellationToken>,
987 progress: Option<&dyn LeanWorkerProgressSink>,
988 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
989 let config = self.session_config.with_imports(imports);
990 self.worker.open_session(&config, cancellation, progress)
991 }
992
993 #[must_use]
995 pub fn dylib_path(&self) -> &Path {
996 &self.dylib_path
997 }
998
999 #[must_use]
1001 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1002 &self.session_config
1003 }
1004
1005 #[must_use]
1007 pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
1008 self.validated_metadata.as_ref()
1009 }
1010
1011 #[must_use]
1013 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1014 self.worker.runtime_metadata()
1015 }
1016
1017 #[must_use]
1019 pub fn stats(&self) -> LeanWorkerStats {
1020 self.worker.stats()
1021 }
1022
1023 #[must_use]
1025 pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1026 self.worker.lifecycle_snapshot()
1027 }
1028
1029 pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1035 self.worker.status()
1036 }
1037
1038 pub fn rss_kib(&mut self) -> Option<u64> {
1040 self.worker.rss_kib()
1041 }
1042
1043 pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1049 self.worker.cycle()
1050 }
1051
1052 pub(crate) fn cycle_with_restart_reason(&mut self, reason: LeanWorkerRestartReason) -> Result<(), LeanWorkerError> {
1053 self.worker.cycle_with_restart_reason(reason)
1054 }
1055
1056 pub(crate) fn record_command_timing(&mut self, first_command_after_open: bool, elapsed: Duration) {
1057 self.worker.record_command_timing(first_command_after_open, elapsed);
1058 }
1059
1060 pub fn set_request_timeout(&mut self, timeout: Duration) {
1062 self.worker.set_request_timeout(timeout);
1063 }
1064
1065 #[doc(hidden)]
1066 pub fn __kill_for_test(&mut self) -> Result<(), LeanWorkerError> {
1072 self.worker.__kill_for_test()
1073 }
1074
1075 #[deprecated(note = "use LeanWorkerCapability::shutdown for structured shutdown status")]
1082 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1083 self.worker.shutdown().map(|report| report.exit)
1084 }
1085
1086 pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1092 self.worker.shutdown()
1093 }
1094}
1095
1096#[derive(Debug)]
1103pub struct LeanWorkerHostHandle {
1104 worker: LeanWorker,
1105 session_config: LeanWorkerSessionConfig,
1106}
1107
1108impl LeanWorkerHostHandle {
1109 pub fn open_session(
1121 &mut self,
1122 cancellation: Option<&LeanWorkerCancellationToken>,
1123 progress: Option<&dyn LeanWorkerProgressSink>,
1124 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1125 self.worker.open_session(&self.session_config, cancellation, progress)
1126 }
1127
1128 pub fn open_session_with_imports(
1138 &mut self,
1139 imports: impl IntoIterator<Item = impl Into<String>>,
1140 cancellation: Option<&LeanWorkerCancellationToken>,
1141 progress: Option<&dyn LeanWorkerProgressSink>,
1142 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1143 let config = self.session_config.with_imports(imports);
1144 self.worker.open_session(&config, cancellation, progress)
1145 }
1146
1147 fn with_session_imports<T>(
1148 &mut self,
1149 imports: Vec<String>,
1150 cancellation: Option<&LeanWorkerCancellationToken>,
1151 progress: Option<&dyn LeanWorkerProgressSink>,
1152 command: impl Fn(&mut LeanWorkerSession<'_>) -> Result<T, LeanWorkerError>,
1153 ) -> Result<T, LeanWorkerError> {
1154 let result = {
1155 let mut session = self.open_session_with_imports(imports.clone(), cancellation, progress)?;
1156 command(&mut session)
1157 };
1158 match result {
1159 Ok(value) => Ok(value),
1160 Err(err) if worker_session_missing(&err) => {
1161 let mut session = self.open_session_with_imports(imports, cancellation, progress)?;
1162 command(&mut session)
1163 }
1164 Err(err) => Err(err),
1165 }
1166 }
1167
1168 pub fn process_module_query_with_imports(
1178 &mut self,
1179 imports: Vec<String>,
1180 source: &str,
1181 query: &LeanWorkerModuleQuery,
1182 options: &LeanWorkerElabOptions,
1183 cancellation: Option<&LeanWorkerCancellationToken>,
1184 progress: Option<&dyn LeanWorkerProgressSink>,
1185 ) -> Result<LeanWorkerModuleQueryOutcome, LeanWorkerError> {
1186 self.with_session_imports(imports, cancellation, progress, |session| {
1187 session.process_module_query(source, query.clone(), options, cancellation, progress)
1188 })
1189 }
1190
1191 pub fn process_module_query_batch_with_imports(
1198 &mut self,
1199 imports: Vec<String>,
1200 source: &str,
1201 selectors: &[LeanWorkerModuleQuerySelector],
1202 budgets: &LeanWorkerOutputBudgets,
1203 options: &LeanWorkerElabOptions,
1204 cancellation: Option<&LeanWorkerCancellationToken>,
1205 progress: Option<&dyn LeanWorkerProgressSink>,
1206 ) -> Result<LeanWorkerModuleQueryBatchOutcome, LeanWorkerError> {
1207 self.with_session_imports(imports, cancellation, progress, |session| {
1208 session.process_module_query_batch(source, selectors, budgets, options, cancellation, progress)
1209 })
1210 }
1211
1212 pub fn inspect_declaration_with_imports(
1218 &mut self,
1219 imports: Vec<String>,
1220 request: &LeanWorkerDeclarationInspectionRequest,
1221 cancellation: Option<&LeanWorkerCancellationToken>,
1222 progress: Option<&dyn LeanWorkerProgressSink>,
1223 ) -> Result<LeanWorkerDeclarationInspectionResult, LeanWorkerError> {
1224 self.with_session_imports(imports, cancellation, progress, |session| {
1225 session.inspect_declaration(request, cancellation, progress)
1226 })
1227 }
1228
1229 pub fn search_declarations_with_imports(
1235 &mut self,
1236 imports: Vec<String>,
1237 search: &LeanWorkerDeclarationSearch,
1238 cancellation: Option<&LeanWorkerCancellationToken>,
1239 progress: Option<&dyn LeanWorkerProgressSink>,
1240 ) -> Result<LeanWorkerDeclarationSearchResult, LeanWorkerError> {
1241 self.with_session_imports(imports, cancellation, progress, |session| {
1242 session.search_declarations(search, cancellation, progress)
1243 })
1244 }
1245
1246 pub fn attempt_proof_with_imports(
1252 &mut self,
1253 imports: Vec<String>,
1254 request: &LeanWorkerProofAttemptRequest,
1255 options: &LeanWorkerElabOptions,
1256 cancellation: Option<&LeanWorkerCancellationToken>,
1257 progress: Option<&dyn LeanWorkerProgressSink>,
1258 ) -> Result<LeanWorkerProofAttemptResult, LeanWorkerError> {
1259 self.with_session_imports(imports, cancellation, progress, |session| {
1260 session.attempt_proof(request, options, cancellation, progress)
1261 })
1262 }
1263
1264 pub fn verify_declaration_with_imports(
1270 &mut self,
1271 imports: Vec<String>,
1272 request: &LeanWorkerDeclarationVerificationRequest,
1273 options: &LeanWorkerElabOptions,
1274 cancellation: Option<&LeanWorkerCancellationToken>,
1275 progress: Option<&dyn LeanWorkerProgressSink>,
1276 ) -> Result<LeanWorkerDeclarationVerificationResult, LeanWorkerError> {
1277 self.with_session_imports(imports, cancellation, progress, |session| {
1278 session.verify_declaration(request, options, cancellation, progress)
1279 })
1280 }
1281
1282 pub fn verify_declaration_batch_with_imports(
1289 &mut self,
1290 imports: Vec<String>,
1291 request: &LeanWorkerDeclarationVerificationBatchRequest,
1292 options: &LeanWorkerElabOptions,
1293 cancellation: Option<&LeanWorkerCancellationToken>,
1294 progress: Option<&dyn LeanWorkerProgressSink>,
1295 ) -> Result<LeanWorkerDeclarationVerificationBatchResult, LeanWorkerError> {
1296 self.with_session_imports(imports, cancellation, progress, |session| {
1297 session.verify_declaration_batch(request, options, cancellation, progress)
1298 })
1299 }
1300
1301 #[must_use]
1303 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1304 &self.session_config
1305 }
1306
1307 #[must_use]
1309 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1310 self.worker.runtime_metadata()
1311 }
1312
1313 #[must_use]
1315 pub fn stats(&self) -> LeanWorkerStats {
1316 self.worker.stats()
1317 }
1318
1319 #[must_use]
1321 pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1322 self.worker.lifecycle_snapshot()
1323 }
1324
1325 pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1331 self.worker.status()
1332 }
1333
1334 pub fn rss_kib(&mut self) -> Option<u64> {
1336 self.worker.rss_kib()
1337 }
1338
1339 pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1345 self.worker.cycle()
1346 }
1347
1348 pub fn restart(&mut self) -> Result<(), LeanWorkerError> {
1354 self.worker.restart()
1355 }
1356
1357 #[deprecated(note = "use LeanWorkerHostHandle::shutdown for structured shutdown status")]
1364 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1365 self.worker.shutdown().map(|report| report.exit)
1366 }
1367
1368 pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1374 self.worker.shutdown()
1375 }
1376}
1377
1378fn worker_session_missing(err: &LeanWorkerError) -> bool {
1379 matches!(err, LeanWorkerError::Worker { code, .. } if code == "lean_rs.worker.session_missing")
1380}
1381
1382#[derive(Clone, Debug)]
1383struct CapabilityMetadataCheck {
1384 export: String,
1385 request: Value,
1386 expected: Option<LeanWorkerCapabilityMetadata>,
1387}
1388
1389#[derive(Debug)]
1390struct WorkerCapabilityArtifact {
1391 dylib_path: PathBuf,
1392 manifest_path: Option<PathBuf>,
1393 package: String,
1394 module: String,
1395}
1396
1397impl WorkerCapabilityArtifact {
1398 fn from_built_capability(spec: &LeanBuiltCapability) -> Result<Self, LeanWorkerError> {
1399 if let Ok(manifest_path) = spec.resolved_manifest_path() {
1400 let mut artifact = Self::from_manifest(&manifest_path)?;
1401 artifact.manifest_path = Some(manifest_path);
1402 return Ok(artifact);
1403 }
1404
1405 let dylib_path = spec.dylib_path().map_err(|err| LeanWorkerError::Setup {
1406 message: err.to_string(),
1407 })?;
1408 let package = spec.package_name().ok_or_else(|| LeanWorkerError::Setup {
1409 message: "LeanBuiltCapability is missing the Lake package name; call `.package(...)`".to_owned(),
1410 })?;
1411 let module = spec.module_name().ok_or_else(|| LeanWorkerError::Setup {
1412 message: "LeanBuiltCapability is missing the root Lean module name; call `.module(...)`".to_owned(),
1413 })?;
1414 Ok(Self {
1415 dylib_path,
1416 manifest_path: None,
1417 package: package.to_owned(),
1418 module: module.to_owned(),
1419 })
1420 }
1421
1422 fn from_manifest(manifest_path: &Path) -> Result<Self, LeanWorkerError> {
1423 let bytes = std::fs::read(manifest_path).map_err(|err| LeanWorkerError::Bootstrap {
1424 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1425 code: LeanLoaderDiagnosticCode::MissingManifest,
1426 },
1427 message: format!(
1428 "could not read Lean capability manifest '{}': {err}",
1429 manifest_path.display()
1430 ),
1431 })?;
1432 let manifest: WorkerCapabilityManifest =
1433 serde_json::from_slice(&bytes).map_err(|err| LeanWorkerError::Bootstrap {
1434 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1435 code: LeanLoaderDiagnosticCode::MalformedManifest,
1436 },
1437 message: format!(
1438 "Lean capability manifest '{}' is malformed: {err}",
1439 manifest_path.display()
1440 ),
1441 })?;
1442 if manifest.schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
1443 return Err(LeanWorkerError::Bootstrap {
1444 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1445 code: LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
1446 },
1447 message: format!(
1448 "unsupported Lean capability manifest schema {}; supported schema is {}",
1449 manifest.schema_version,
1450 lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
1451 ),
1452 });
1453 }
1454 Ok(Self {
1455 dylib_path: manifest.primary_dylib,
1456 manifest_path: Some(manifest_path.to_path_buf()),
1457 package: manifest.package,
1458 module: manifest.module,
1459 })
1460 }
1461}
1462
1463#[derive(Deserialize)]
1464struct WorkerCapabilityManifest {
1465 schema_version: u64,
1466 primary_dylib: PathBuf,
1467 package: String,
1468 module: String,
1469}
1470
1471fn worker_child_static_checks(worker_child: Option<&LeanWorkerChild>) -> Vec<LeanWorkerBootstrapCheck> {
1472 let mut checks = Vec::new();
1473 match worker_child.map_or_else(resolve_default_worker_executable, LeanWorkerChild::resolve) {
1474 Ok(path) => {
1475 if let Err(err) = validate_worker_child_path(&path) {
1476 checks.push(check_from_open_error(&err));
1477 }
1478 }
1479 Err(err) => checks.push(check_from_open_error(&err)),
1480 }
1481 checks
1482}
1483
1484fn spawn_checked_worker(
1485 worker_child: Option<LeanWorkerChild>,
1486 startup_timeout: Option<Duration>,
1487 request_timeout: Option<Duration>,
1488 shutdown_timeout: Option<Duration>,
1489 restart_policy: Option<LeanWorkerRestartPolicy>,
1490 rss_hard_limit: Option<(u64, Duration)>,
1491 module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
1492 max_frame_bytes: Option<u32>,
1493) -> Result<LeanWorker, LeanWorkerError> {
1494 let worker_child = worker_child.unwrap_or_default();
1495 let worker_executable = worker_child.resolve()?;
1496 validate_worker_child_path(&worker_executable)?;
1497 let lean_sysroot = worker_child.resolve_lean_sysroot()?;
1498
1499 let mut config = LeanWorkerConfig::new(worker_executable).env("LEAN_SYSROOT", lean_sysroot.as_os_str());
1500 if let Some(timeout) = startup_timeout {
1501 config = config.startup_timeout(timeout);
1502 }
1503 if let Some(timeout) = request_timeout {
1504 config = config.request_timeout(timeout);
1505 }
1506 if let Some(timeout) = shutdown_timeout {
1507 config = config.shutdown_timeout(timeout);
1508 }
1509 if let Some(policy) = restart_policy {
1510 config = config.restart_policy(policy);
1511 }
1512 if let Some((limit_kib, sample_interval)) = rss_hard_limit {
1513 config = config.rss_hard_limit(limit_kib, sample_interval);
1514 }
1515 if let Some(limits) = module_cache_limits.as_ref() {
1516 config = apply_module_cache_limits(config, limits);
1517 }
1518 if let Some(cap) = max_frame_bytes {
1519 config = config.max_frame_bytes(cap);
1520 }
1521
1522 let mut worker = LeanWorker::spawn(&config)?;
1523 worker.health()?;
1524 Ok(worker)
1525}
1526
1527fn apply_module_cache_limits(mut config: LeanWorkerConfig, limits: &LeanWorkerModuleCacheLimits) -> LeanWorkerConfig {
1528 if let Some(value) = limits.max_entries {
1529 config = config.env("LEAN_RS_MODULE_CACHE_MAX_ENTRIES", value.to_string());
1530 }
1531 if let Some(value) = limits.ttl_millis {
1532 config = config.env("LEAN_RS_MODULE_CACHE_TTL_MILLIS", value.to_string());
1533 }
1534 if let Some(value) = limits.max_bytes {
1535 config = config.env("LEAN_RS_MODULE_CACHE_MAX_BYTES", value.to_string());
1536 }
1537 if let Some(value) = limits.rss_guard_kib {
1538 config = config.env("LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB", value.to_string());
1539 }
1540 if let Some(value) = limits.verify_rss_taint_kib {
1541 config = config.env("LEAN_RS_VERIFY_RSS_TAINT_KIB", value.to_string());
1542 }
1543 config
1544}
1545
1546#[derive(Clone, Debug, Eq, PartialEq)]
1596pub struct LeanWorkerChild {
1597 executable_name: Option<String>,
1598 explicit_path: Option<PathBuf>,
1599 env_var: Option<String>,
1600 lean_sysroot: Option<PathBuf>,
1601}
1602
1603impl LeanWorkerChild {
1604 #[must_use]
1614 pub fn sibling(executable_name: impl Into<String>) -> Self {
1615 Self {
1616 executable_name: Some(with_exe_suffix(executable_name.into())),
1617 explicit_path: None,
1618 env_var: None,
1619 lean_sysroot: None,
1620 }
1621 }
1622
1623 #[must_use]
1625 pub fn path(path: impl Into<PathBuf>) -> Self {
1626 Self {
1627 executable_name: None,
1628 explicit_path: Some(path.into()),
1629 env_var: None,
1630 lean_sysroot: None,
1631 }
1632 }
1633
1634 #[must_use]
1642 pub fn for_toolchain(path: impl Into<PathBuf>, sysroot: impl Into<PathBuf>) -> Self {
1643 Self {
1644 executable_name: None,
1645 explicit_path: Some(path.into()),
1646 env_var: None,
1647 lean_sysroot: Some(sysroot.into()),
1648 }
1649 }
1650
1651 #[must_use]
1656 pub fn lean_sysroot(mut self, sysroot: impl Into<PathBuf>) -> Self {
1657 self.lean_sysroot = Some(sysroot.into());
1658 self
1659 }
1660
1661 #[must_use]
1663 pub fn env_override(mut self, env_var: impl Into<String>) -> Self {
1664 self.env_var = Some(env_var.into());
1665 self
1666 }
1667
1668 fn resolve_lean_sysroot(&self) -> Result<PathBuf, LeanWorkerError> {
1675 if let Some(sysroot) = &self.lean_sysroot {
1676 return Ok(sysroot.clone());
1677 }
1678 let info = lean_toolchain::discover_toolchain(&lean_toolchain::DiscoverOptions::default()).map_err(|diag| {
1679 LeanWorkerError::Setup {
1680 message: format!("could not discover Lean sysroot for worker spawn: {diag}"),
1681 }
1682 })?;
1683 Ok(info.prefix)
1684 }
1685
1686 fn resolve(&self) -> Result<PathBuf, LeanWorkerError> {
1687 let mut tried = Vec::new();
1688 if let Some(env_var) = &self.env_var
1689 && let Some(value) = env::var_os(env_var)
1690 {
1691 let path = PathBuf::from(value);
1692 if path.is_file() {
1693 return Ok(path);
1694 }
1695 tried.push(path);
1696 return Err(LeanWorkerError::WorkerChildUnresolved { tried });
1697 }
1698 if let Some(path) = &self.explicit_path {
1699 return Ok(path.clone());
1700 }
1701
1702 let executable_name = self
1703 .executable_name
1704 .clone()
1705 .unwrap_or_else(|| with_exe_suffix("lean-rs-worker-child".to_owned()));
1706 tried.extend(candidate_sibling_worker_paths(&executable_name));
1707 if executable_name == with_exe_suffix("lean-rs-worker-child".to_owned())
1712 && let Some(path) = try_build_workspace_worker_child(&executable_name, &mut tried)
1713 {
1714 return Ok(path);
1715 }
1716 for path in dedup_paths(&tried) {
1717 if path.is_file() {
1718 return Ok(path);
1719 }
1720 }
1721 Err(LeanWorkerError::WorkerChildUnresolved { tried })
1722 }
1723}
1724
1725impl Default for LeanWorkerChild {
1726 fn default() -> Self {
1727 Self::sibling("lean-rs-worker-child").env_override(WORKER_CHILD_ENV)
1728 }
1729}
1730
1731fn resolve_default_worker_executable() -> Result<PathBuf, LeanWorkerError> {
1732 LeanWorkerChild::default().resolve()
1733}
1734
1735fn validate_worker_child_path(path: &Path) -> Result<(), LeanWorkerError> {
1736 if !path.is_file() {
1737 return Err(LeanWorkerError::WorkerChildNotExecutable {
1738 path: path.to_path_buf(),
1739 reason: "path does not point to a file".to_owned(),
1740 });
1741 }
1742 if !is_executable_file(path) {
1743 return Err(LeanWorkerError::WorkerChildNotExecutable {
1744 path: path.to_path_buf(),
1745 reason: "file is not executable by this user".to_owned(),
1746 });
1747 }
1748 Ok(())
1749}
1750
1751#[cfg(unix)]
1752fn is_executable_file(path: &Path) -> bool {
1753 use std::os::unix::fs::PermissionsExt as _;
1754
1755 std::fs::metadata(path).is_ok_and(|metadata| metadata.permissions().mode() & 0o111 != 0)
1756}
1757
1758#[cfg(not(unix))]
1759fn is_executable_file(_path: &Path) -> bool {
1760 true
1761}
1762
1763fn check_from_open_error(err: &LeanWorkerError) -> LeanWorkerBootstrapCheck {
1764 match err {
1765 LeanWorkerError::WorkerChildUnresolved { tried } => LeanWorkerBootstrapCheck::error(
1766 LeanWorkerBootstrapDiagnosticCode::WorkerChildUnresolved,
1767 "worker child",
1768 format!("could not resolve worker child; tried {}", format_paths(tried)),
1769 "ship an app-owned worker child binary beside the app or configure LeanWorkerChild::env_override",
1770 ),
1771 LeanWorkerError::WorkerChildNotExecutable { path, reason } => LeanWorkerBootstrapCheck::error(
1772 LeanWorkerBootstrapDiagnosticCode::WorkerChildNotExecutable,
1773 path.display().to_string(),
1774 reason.clone(),
1775 "ship an app-owned worker child binary and ensure it is executable",
1776 ),
1777 LeanWorkerError::Bootstrap { code, message } => LeanWorkerBootstrapCheck::error(
1778 *code,
1779 code.as_str(),
1780 message.clone(),
1781 "fix the reported bootstrap input",
1782 ),
1783 LeanWorkerError::Handshake { message } => LeanWorkerBootstrapCheck::error(
1784 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1785 "worker handshake",
1786 message.clone(),
1787 "ensure the worker child calls lean_rs_worker_child::run_worker_child_stdio and matches this crate version",
1788 ),
1789 LeanWorkerError::Timeout {
1790 operation: "startup", ..
1791 } => LeanWorkerBootstrapCheck::error(
1792 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1793 "worker handshake",
1794 err.to_string(),
1795 "check that the worker child starts promptly and writes the lean-rs-worker handshake",
1796 ),
1797 LeanWorkerError::CapabilityMetadataMismatch { export, .. } => LeanWorkerBootstrapCheck::error(
1798 LeanWorkerBootstrapDiagnosticCode::CapabilityMetadataMismatch,
1799 export.clone(),
1800 "capability metadata did not match the requested expectation",
1801 "rebuild or select a capability whose metadata matches the caller expectation",
1802 ),
1803 other @ (LeanWorkerError::Spawn { .. }
1804 | LeanWorkerError::CapabilityBuild { .. }
1805 | LeanWorkerError::Setup { .. }
1806 | LeanWorkerError::Protocol { .. }
1807 | LeanWorkerError::Worker { .. }
1808 | LeanWorkerError::ChildExited { .. }
1809 | LeanWorkerError::ChildPanicOrAbort { .. }
1810 | LeanWorkerError::Timeout { .. }
1811 | LeanWorkerError::RssHardLimitExceeded { .. }
1812 | LeanWorkerError::Cancelled { .. }
1813 | LeanWorkerError::ProgressPanic { .. }
1814 | LeanWorkerError::DataSinkPanic { .. }
1815 | LeanWorkerError::DiagnosticSinkPanic { .. }
1816 | LeanWorkerError::StreamExportFailed { .. }
1817 | LeanWorkerError::StreamCallbackFailed { .. }
1818 | LeanWorkerError::StreamRowMalformed { .. }
1819 | LeanWorkerError::CapabilityMetadataMalformed { .. }
1820 | LeanWorkerError::CapabilityDoctorMalformed { .. }
1821 | LeanWorkerError::TypedCommandRequestEncode { .. }
1822 | LeanWorkerError::TypedCommandResponseDecode { .. }
1823 | LeanWorkerError::TypedCommandRowDecode { .. }
1824 | LeanWorkerError::TypedCommandSummaryDecode { .. }
1825 | LeanWorkerError::LeaseInvalidated { .. }
1826 | LeanWorkerError::WorkerPoolExhausted { .. }
1827 | LeanWorkerError::WorkerPoolMemoryBudgetExceeded { .. }
1828 | LeanWorkerError::WorkerPoolQueueTimeout { .. }
1829 | LeanWorkerError::RestartLimitExceeded { .. }
1830 | LeanWorkerError::UnsupportedRequest { .. }
1831 | LeanWorkerError::Wait { .. }
1832 | LeanWorkerError::Kill { .. }
1833 | LeanWorkerError::WaitTimeout { .. }
1834 | LeanWorkerError::ShutdownInProgress { .. }) => LeanWorkerBootstrapCheck::error(
1835 LeanWorkerBootstrapDiagnosticCode::WorkerStartupFailed,
1836 "worker bootstrap",
1837 other.to_string(),
1838 "run the bootstrap check in a deployment environment and rebuild the worker child or capability artifact",
1839 ),
1840 }
1841}
1842
1843fn format_paths(paths: &[PathBuf]) -> String {
1844 if paths.is_empty() {
1845 return "<none>".to_owned();
1846 }
1847 paths
1848 .iter()
1849 .map(|path| path.display().to_string())
1850 .collect::<Vec<_>>()
1851 .join(", ")
1852}
1853
1854fn bound_bootstrap_text(mut text: String) -> String {
1855 const LIMIT: usize = 1_024;
1856 if text.len() <= LIMIT {
1857 return text;
1858 }
1859 while !text.is_char_boundary(LIMIT) {
1860 text.pop();
1861 }
1862 text.truncate(LIMIT);
1863 text.push_str("...");
1864 text
1865}
1866
1867fn candidate_sibling_worker_paths(executable_name: &str) -> Vec<PathBuf> {
1868 let mut tried = Vec::new();
1869 if let Ok(current_exe) = env::current_exe() {
1870 if let Some(dir) = current_exe.parent() {
1871 tried.push(dir.join(executable_name));
1872 }
1873 if let Some(profile_dir) = current_exe.parent().and_then(Path::parent) {
1874 tried.push(profile_dir.join(executable_name));
1875 }
1876 }
1877 tried
1878}
1879
1880fn with_exe_suffix(mut executable_name: String) -> String {
1881 if !env::consts::EXE_SUFFIX.is_empty() && !executable_name.ends_with(env::consts::EXE_SUFFIX) {
1882 executable_name.push_str(env::consts::EXE_SUFFIX);
1883 }
1884 executable_name
1885}
1886
1887fn infer_lake_project_root_from_dylib(dylib_path: &Path) -> Result<PathBuf, LeanWorkerError> {
1888 let lib_dir = dylib_path.parent();
1889 let build_dir = lib_dir.and_then(Path::parent);
1890 let lake_dir = build_dir.and_then(Path::parent);
1891 let project_root = lake_dir.and_then(Path::parent);
1892 match (lib_dir, build_dir, lake_dir, project_root) {
1893 (Some(lib), Some(build), Some(lake), Some(root))
1894 if lib.file_name().is_some_and(|name| name == "lib")
1895 && build.file_name().is_some_and(|name| name == "build")
1896 && lake.file_name().is_some_and(|name| name == ".lake") =>
1897 {
1898 Ok(root.to_path_buf())
1899 }
1900 _ => Err(LeanWorkerError::Setup {
1901 message: format!(
1902 "built capability dylib '{}' is not under a standard .lake/build/lib directory",
1903 dylib_path.display()
1904 ),
1905 }),
1906 }
1907}
1908
1909fn normalize_import_workspace_root(path: PathBuf) -> PathBuf {
1910 std::fs::canonicalize(&path).unwrap_or(path)
1911}
1912
1913fn try_build_workspace_worker_child(executable_name: &str, tried: &mut Vec<PathBuf>) -> Option<PathBuf> {
1924 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1925 let workspace = manifest_dir.parent()?.parent()?;
1926 if !workspace
1927 .join("crates")
1928 .join("lean-rs-worker-child")
1929 .join("Cargo.toml")
1930 .is_file()
1931 {
1932 return None;
1933 }
1934
1935 let debug = workspace.join("target").join("debug").join(executable_name);
1936 let release = workspace.join("target").join("release").join(executable_name);
1937 tried.push(debug.clone());
1938 tried.push(release.clone());
1939 if debug.is_file() {
1940 return Some(debug);
1941 }
1942 if release.is_file() {
1943 return Some(release);
1944 }
1945
1946 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
1947 let status = Command::new(cargo)
1948 .current_dir(workspace)
1949 .args(["build", "-p", "lean-rs-worker-child", "--bin", "lean-rs-worker-child"])
1950 .status()
1951 .ok()?;
1952 if !status.success() {
1953 return None;
1954 }
1955 debug.is_file().then_some(debug)
1956}
1957
1958fn dedup_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
1959 let mut unique = Vec::new();
1960 for path in paths {
1961 if !unique.iter().any(|existing| existing == path) {
1962 unique.push(path.clone());
1963 }
1964 }
1965 unique
1966}
1967
1968#[cfg(test)]
1969#[allow(clippy::expect_used, clippy::panic)]
1970mod tests {
1971 use super::{LeanWorkerCapabilityBuilder, LeanWorkerChild, LeanWorkerModuleCacheLimits, apply_module_cache_limits};
1972 use crate::supervisor::LeanWorkerConfig;
1973 use lean_rs_worker_protocol::types::LeanWorkerSessionImportProfile;
1974 use lean_toolchain::LeanBuiltCapability;
1975 use std::path::PathBuf;
1976
1977 fn workspace_root() -> PathBuf {
1978 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1979 manifest_dir
1980 .parent()
1981 .and_then(std::path::Path::parent)
1982 .expect("crates/<name> lives two directories below the workspace root")
1983 .to_path_buf()
1984 }
1985
1986 fn interop_root() -> PathBuf {
1987 workspace_root().join("fixtures").join("interop-shims")
1988 }
1989
1990 fn capability_builder() -> LeanWorkerCapabilityBuilder {
1991 LeanWorkerCapabilityBuilder::new(
1992 interop_root(),
1993 "lean_rs_interop_consumer",
1994 "LeanRsInteropConsumer",
1995 ["LeanRsInteropConsumer.Callback"],
1996 )
1997 }
1998
1999 #[test]
2000 fn import_workspace_root_unset_matches_capability_project_root() {
2001 let key = capability_builder().session_key();
2002 assert_eq!(key.project_root(), interop_root().as_path());
2003 assert_eq!(key.import_workspace_root(), interop_root().as_path());
2004 }
2005
2006 #[test]
2007 fn project_root_is_canonicalized_for_session_reuse() {
2008 assert_eq!(
2009 capability_builder().session_key(),
2010 LeanWorkerCapabilityBuilder::new(
2011 interop_root().join("."),
2012 "lean_rs_interop_consumer",
2013 "LeanRsInteropConsumer",
2014 ["LeanRsInteropConsumer.Callback"],
2015 )
2016 .session_key(),
2017 );
2018 }
2019
2020 #[test]
2021 fn explicit_import_workspace_root_matching_capability_root_preserves_key() {
2022 assert_eq!(
2023 capability_builder().session_key(),
2024 capability_builder().import_workspace_root(interop_root()).session_key()
2025 );
2026 }
2027
2028 #[test]
2029 fn import_workspace_root_is_canonicalized_for_session_reuse() {
2030 assert_eq!(
2031 capability_builder().import_workspace_root(interop_root()).session_key(),
2032 capability_builder()
2033 .import_workspace_root(interop_root().join("."))
2034 .session_key(),
2035 );
2036 }
2037
2038 #[test]
2039 fn import_workspace_root_participates_in_session_key() {
2040 assert_ne!(
2041 capability_builder().session_key(),
2042 capability_builder()
2043 .import_workspace_root(workspace_root())
2044 .session_key(),
2045 );
2046 }
2047
2048 #[test]
2049 fn import_profile_participates_in_session_key() {
2050 assert_ne!(
2051 capability_builder().session_key(),
2052 capability_builder()
2053 .import_profile(LeanWorkerSessionImportProfile::FullPrivateCompat)
2054 .session_key(),
2055 );
2056 }
2057
2058 #[test]
2059 fn built_manifest_path_participates_in_session_key() {
2060 let manifest_dir = std::env::temp_dir().join(format!("lean-rs-worker-manifest-key-{}", std::process::id()));
2061 std::fs::create_dir_all(&manifest_dir).expect("manifest temp dir");
2062 let dylib = interop_root()
2063 .join(".lake")
2064 .join("build")
2065 .join("lib")
2066 .join(if cfg!(target_os = "macos") {
2067 "liblean__rs__interop__consumer_LeanRsInteropConsumer.dylib"
2068 } else {
2069 "liblean__rs__interop__consumer_LeanRsInteropConsumer.so"
2070 });
2071 let manifest_a = manifest_dir.join("a.json");
2072 let manifest_b = manifest_dir.join("b.json");
2073 for manifest in [&manifest_a, &manifest_b] {
2074 std::fs::write(
2075 manifest,
2076 format!(
2077 r#"{{"schema_version":2,"primary_dylib":{},"package":"lean_rs_interop_consumer","module":"LeanRsInteropConsumer"}}"#,
2078 serde_json::to_string(&dylib).expect("dylib path json")
2079 ),
2080 )
2081 .expect("write manifest");
2082 }
2083
2084 let key_a = LeanWorkerCapabilityBuilder::from_built_capability(
2085 &LeanBuiltCapability::manifest_path(&manifest_a),
2086 ["LeanRsInteropConsumer.Callback"],
2087 )
2088 .expect("manifest A accepted")
2089 .session_key();
2090 let key_a_dot = LeanWorkerCapabilityBuilder::from_built_capability(
2091 &LeanBuiltCapability::manifest_path(manifest_dir.join(".").join("a.json")),
2092 ["LeanRsInteropConsumer.Callback"],
2093 )
2094 .expect("canonical-equivalent manifest accepted")
2095 .session_key();
2096 let key_b = LeanWorkerCapabilityBuilder::from_built_capability(
2097 &LeanBuiltCapability::manifest_path(&manifest_b),
2098 ["LeanRsInteropConsumer.Callback"],
2099 )
2100 .expect("manifest B accepted")
2101 .session_key();
2102
2103 assert_eq!(key_a, key_a_dot);
2104 assert_ne!(key_a, key_b);
2105 drop(std::fs::remove_dir_all(manifest_dir));
2106 }
2107
2108 #[test]
2109 fn for_toolchain_carries_sysroot_through_resolve() {
2110 let sysroot = PathBuf::from("/opt/some/lean/prefix");
2111 let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2112 let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2113 assert_eq!(resolved, sysroot);
2114 }
2115
2116 #[test]
2117 fn lean_sysroot_setter_overrides_default() {
2118 let sysroot = PathBuf::from("/opt/override/lean");
2119 let child = LeanWorkerChild::path("/opt/worker").lean_sysroot(&sysroot);
2120 let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2121 assert_eq!(resolved, sysroot);
2122 }
2123
2124 #[test]
2125 fn explicit_sysroot_bypasses_discovery_even_when_path_is_nonexistent() {
2126 let sysroot = PathBuf::from("/definitely/not/a/real/sysroot");
2131 let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2132 let resolved = child
2133 .resolve_lean_sysroot()
2134 .expect("explicit sysroot resolves without filesystem checks");
2135 assert_eq!(resolved, sysroot);
2136 }
2137
2138 #[test]
2139 fn module_cache_limits_map_to_typed_child_policy_env() {
2140 let limits = LeanWorkerModuleCacheLimits::default()
2141 .max_entries(7)
2142 .ttl(std::time::Duration::from_millis(250))
2143 .max_bytes(4096)
2144 .rss_guard_kib(8192);
2145 let config = apply_module_cache_limits(LeanWorkerConfig::new("/opt/worker"), &limits);
2146 let env = config.env_overrides();
2147 assert!(
2148 env.iter()
2149 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_ENTRIES" && v == "7")
2150 );
2151 assert!(
2152 env.iter()
2153 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_TTL_MILLIS" && v == "250")
2154 );
2155 assert!(
2156 env.iter()
2157 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_BYTES" && v == "4096")
2158 );
2159 assert!(
2160 env.iter()
2161 .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB" && v == "8192")
2162 );
2163 }
2164}