1use std::env;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::time::Duration;
11
12use lean_rs::{LeanBuiltCapability, LeanCapabilityPreflight, LeanLoaderDiagnosticCode};
13use serde::Deserialize;
14use serde_json::Value;
15
16use crate::pool::{LeanWorkerRestartPolicyClass, LeanWorkerSessionKey};
17use crate::session::{
18 LeanWorkerCancellationToken, LeanWorkerCapabilityMetadata, LeanWorkerProgressSink, LeanWorkerRuntimeMetadata,
19 LeanWorkerSession, LeanWorkerSessionConfig,
20};
21use crate::supervisor::{
22 LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING, LeanWorker, LeanWorkerConfig, LeanWorkerError, LeanWorkerRestartPolicy,
23};
24
25const WORKER_CHILD_ENV: &str = "LEAN_RS_WORKER_CHILD";
26
27#[derive(Clone, Debug)]
42pub struct LeanWorkerCapabilityBuilder {
43 project_root: PathBuf,
44 package: String,
45 lib_name: String,
46 imports: Vec<String>,
47 built_dylib_path: Option<PathBuf>,
48 built_capability: Option<LeanBuiltCapability>,
49 worker_child: Option<LeanWorkerChild>,
50 startup_timeout: Option<Duration>,
51 request_timeout: Option<Duration>,
52 restart_policy: Option<LeanWorkerRestartPolicy>,
53 metadata_check: Option<CapabilityMetadataCheck>,
54}
55
56impl LeanWorkerCapabilityBuilder {
57 #[must_use]
63 pub fn new(
64 project_root: impl Into<PathBuf>,
65 package: impl Into<String>,
66 lib_name: impl Into<String>,
67 imports: impl IntoIterator<Item = impl Into<String>>,
68 ) -> Self {
69 Self {
70 project_root: project_root.into(),
71 package: package.into(),
72 lib_name: lib_name.into(),
73 imports: imports.into_iter().map(Into::into).collect(),
74 built_dylib_path: None,
75 built_capability: None,
76 worker_child: None,
77 startup_timeout: None,
78 request_timeout: None,
79 restart_policy: None,
80 metadata_check: None,
81 }
82 }
83
84 pub fn from_built_capability(
100 spec: &LeanBuiltCapability,
101 imports: impl IntoIterator<Item = impl Into<String>>,
102 ) -> Result<Self, LeanWorkerError> {
103 let artifact = WorkerCapabilityArtifact::from_built_capability(spec)?;
104 let project_root = infer_lake_project_root_from_dylib(&artifact.dylib_path)?;
105 Ok(Self {
106 project_root,
107 package: artifact.package,
108 lib_name: artifact.module,
109 imports: imports.into_iter().map(Into::into).collect(),
110 built_dylib_path: Some(artifact.dylib_path),
111 built_capability: Some(spec.clone()),
112 worker_child: None,
113 startup_timeout: None,
114 request_timeout: None,
115 restart_policy: None,
116 metadata_check: None,
117 })
118 }
119
120 #[must_use]
125 pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
126 self.worker_child = Some(LeanWorkerChild::path(path));
127 self
128 }
129
130 #[must_use]
132 pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
133 self.worker_child = Some(child);
134 self
135 }
136
137 #[must_use]
139 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
140 self.startup_timeout = Some(timeout);
141 self
142 }
143
144 #[must_use]
146 pub fn request_timeout(mut self, timeout: Duration) -> Self {
147 self.request_timeout = Some(timeout);
148 self
149 }
150
151 #[must_use]
153 pub fn long_running_requests(mut self) -> Self {
154 self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
155 self
156 }
157
158 #[must_use]
160 pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
161 self.restart_policy = Some(policy);
162 self
163 }
164
165 #[must_use]
171 pub fn validate_metadata(mut self, export: impl Into<String>, request: Value) -> Self {
172 self.metadata_check = Some(CapabilityMetadataCheck {
173 export: export.into(),
174 request,
175 expected: None,
176 });
177 self
178 }
179
180 #[must_use]
186 pub fn expect_metadata(
187 mut self,
188 export: impl Into<String>,
189 request: Value,
190 expected: LeanWorkerCapabilityMetadata,
191 ) -> Self {
192 self.metadata_check = Some(CapabilityMetadataCheck {
193 export: export.into(),
194 request,
195 expected: Some(expected),
196 });
197 self
198 }
199
200 #[must_use]
206 pub fn session_key(&self) -> LeanWorkerSessionKey {
207 let restart_policy_class = match &self.restart_policy {
208 Some(policy) if policy == &LeanWorkerRestartPolicy::default() => LeanWorkerRestartPolicyClass::Default,
209 Some(_policy) => LeanWorkerRestartPolicyClass::Custom,
210 None => LeanWorkerRestartPolicyClass::Default,
211 };
212 let mut key = LeanWorkerSessionKey::new(
213 self.project_root.clone(),
214 self.package.clone(),
215 self.lib_name.clone(),
216 self.imports.clone(),
217 )
218 .restart_policy_class(restart_policy_class);
219 if let Some(check) = &self.metadata_check {
220 key = key.metadata_expectation(check.export.clone(), check.request.clone(), check.expected.clone());
221 }
222 key
223 }
224
225 pub(crate) fn pool_request_timeout(&self) -> Duration {
226 self.request_timeout
227 .unwrap_or(crate::supervisor::LEAN_WORKER_REQUEST_TIMEOUT_DEFAULT)
228 }
229
230 #[must_use]
238 pub fn check(&self) -> LeanWorkerBootstrapReport {
239 let mut checks = self.bootstrap_static_checks();
240 if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
241 return LeanWorkerBootstrapReport::new(checks);
242 }
243
244 match self.clone().open_unchecked() {
245 Ok(capability) => {
246 drop(capability.terminate());
247 }
248 Err(err) => checks.push(check_from_open_error(&err)),
249 }
250 LeanWorkerBootstrapReport::new(checks)
251 }
252
253 fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
254 let mut checks = Vec::new();
255 match self
256 .worker_child
257 .as_ref()
258 .map_or_else(resolve_default_worker_executable, LeanWorkerChild::resolve)
259 {
260 Ok(path) => {
261 if let Err(err) = validate_worker_child_path(&path) {
262 checks.push(check_from_open_error(&err));
263 }
264 }
265 Err(err) => checks.push(check_from_open_error(&err)),
266 }
267
268 if let Some(spec) = &self.built_capability
269 && spec.resolved_manifest_path().is_ok()
270 {
271 let report = LeanCapabilityPreflight::new(spec.clone()).check();
272 for check in report.errors() {
273 checks.push(LeanWorkerBootstrapCheck::error(
274 LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight { code: check.code() },
275 check.subject().to_owned(),
276 check.message().to_owned(),
277 check.repair_hint().to_owned(),
278 ));
279 }
280 }
281 checks
282 }
283
284 pub fn open(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
292 let report = self.bootstrap_static_report();
293 if let Some(check) = report.first_error() {
294 return Err(LeanWorkerError::Bootstrap {
295 code: check.code(),
296 message: check.message().to_owned(),
297 });
298 }
299 self.open_unchecked()
300 }
301
302 fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
303 LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
304 }
305
306 fn open_unchecked(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
307 let dylib_path = match self.built_dylib_path {
308 Some(path) => path,
309 None => lean_toolchain::build_lake_target_quiet(&self.project_root, &self.lib_name)
310 .map_err(|diagnostic| LeanWorkerError::CapabilityBuild { diagnostic })?,
311 };
312 let worker_executable = self
313 .worker_child
314 .map_or_else(resolve_default_worker_executable, |child| child.resolve())?;
315 validate_worker_child_path(&worker_executable)?;
316
317 let mut config = LeanWorkerConfig::new(worker_executable);
318 if let Some(timeout) = self.startup_timeout {
319 config = config.startup_timeout(timeout);
320 }
321 if let Some(timeout) = self.request_timeout {
322 config = config.request_timeout(timeout);
323 }
324 if let Some(policy) = self.restart_policy {
325 config = config.restart_policy(policy);
326 }
327
328 let mut worker = LeanWorker::spawn(&config)?;
329 worker.health()?;
330
331 let session_config = LeanWorkerSessionConfig::new(
332 self.project_root.clone(),
333 self.package.clone(),
334 self.lib_name.clone(),
335 self.imports.clone(),
336 );
337
338 let validated_metadata = {
339 let mut session = worker.open_session(&session_config, None, None)?;
340 match self.metadata_check {
341 Some(check) => {
342 let metadata = session.capability_metadata(&check.export, &check.request, None, None)?;
343 if let Some(expected) = check.expected
344 && metadata != expected
345 {
346 return Err(LeanWorkerError::CapabilityMetadataMismatch {
347 export: check.export,
348 expected: Box::new(expected),
349 actual: Box::new(metadata),
350 });
351 }
352 Some(metadata)
353 }
354 None => None,
355 }
356 };
357
358 Ok(LeanWorkerCapability {
359 worker,
360 session_config,
361 dylib_path,
362 validated_metadata,
363 })
364 }
365}
366
367#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
369#[non_exhaustive]
370pub enum LeanWorkerBootstrapDiagnosticCode {
371 WorkerChildUnresolved,
373 WorkerChildNotExecutable,
375 CapabilityPreflight { code: LeanLoaderDiagnosticCode },
377 WorkerHandshakeFailed,
379 CapabilityMetadataMismatch,
381 WorkerStartupFailed,
383}
384
385impl LeanWorkerBootstrapDiagnosticCode {
386 #[must_use]
388 pub const fn as_str(self) -> &'static str {
389 match self {
390 Self::WorkerChildUnresolved => "lean_rs.worker.bootstrap.child_unresolved",
391 Self::WorkerChildNotExecutable => "lean_rs.worker.bootstrap.child_not_executable",
392 Self::CapabilityPreflight { code } => code.as_str(),
393 Self::WorkerHandshakeFailed => "lean_rs.worker.bootstrap.handshake_failed",
394 Self::CapabilityMetadataMismatch => "lean_rs.worker.bootstrap.metadata_mismatch",
395 Self::WorkerStartupFailed => "lean_rs.worker.bootstrap.startup_failed",
396 }
397 }
398}
399
400impl std::fmt::Display for LeanWorkerBootstrapDiagnosticCode {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 f.write_str(self.as_str())
403 }
404}
405
406#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
408#[non_exhaustive]
409pub enum LeanWorkerBootstrapSeverity {
410 Info,
412 Warning,
414 Error,
416}
417
418#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct LeanWorkerBootstrapCheck {
421 code: LeanWorkerBootstrapDiagnosticCode,
422 severity: LeanWorkerBootstrapSeverity,
423 subject: String,
424 message: String,
425 repair_hint: String,
426}
427
428impl LeanWorkerBootstrapCheck {
429 fn error(
430 code: LeanWorkerBootstrapDiagnosticCode,
431 subject: impl Into<String>,
432 message: impl Into<String>,
433 repair_hint: impl Into<String>,
434 ) -> Self {
435 Self {
436 code,
437 severity: LeanWorkerBootstrapSeverity::Error,
438 subject: bound_bootstrap_text(subject.into()),
439 message: bound_bootstrap_text(message.into()),
440 repair_hint: bound_bootstrap_text(repair_hint.into()),
441 }
442 }
443
444 #[must_use]
446 pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
447 self.code
448 }
449
450 #[must_use]
452 pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
453 self.severity
454 }
455
456 #[must_use]
458 pub fn subject(&self) -> &str {
459 &self.subject
460 }
461
462 #[must_use]
464 pub fn message(&self) -> &str {
465 &self.message
466 }
467
468 #[must_use]
470 pub fn repair_hint(&self) -> &str {
471 &self.repair_hint
472 }
473
474 fn is_error(&self) -> bool {
475 self.severity == LeanWorkerBootstrapSeverity::Error
476 }
477}
478
479#[derive(Clone, Debug, Eq, PartialEq)]
481pub struct LeanWorkerBootstrapReport {
482 checks: Vec<LeanWorkerBootstrapCheck>,
483}
484
485impl LeanWorkerBootstrapReport {
486 fn new(checks: Vec<LeanWorkerBootstrapCheck>) -> Self {
487 Self { checks }
488 }
489
490 #[must_use]
492 pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
493 &self.checks
494 }
495
496 pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
498 self.checks
499 .iter()
500 .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
501 }
502
503 #[must_use]
505 pub fn is_ok(&self) -> bool {
506 self.first_error().is_none()
507 }
508
509 #[must_use]
511 pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
512 self.errors().next()
513 }
514}
515
516#[derive(Debug)]
522pub struct LeanWorkerCapability {
523 worker: LeanWorker,
524 session_config: LeanWorkerSessionConfig,
525 dylib_path: PathBuf,
526 validated_metadata: Option<LeanWorkerCapabilityMetadata>,
527}
528
529impl LeanWorkerCapability {
530 pub fn open_session(
542 &mut self,
543 cancellation: Option<&LeanWorkerCancellationToken>,
544 progress: Option<&dyn LeanWorkerProgressSink>,
545 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
546 self.worker.open_session(&self.session_config, cancellation, progress)
547 }
548
549 pub fn open_session_with_imports(
560 &mut self,
561 imports: impl IntoIterator<Item = impl Into<String>>,
562 cancellation: Option<&LeanWorkerCancellationToken>,
563 progress: Option<&dyn LeanWorkerProgressSink>,
564 ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
565 let config = LeanWorkerSessionConfig::new(
566 self.session_config.project_root_string(),
567 self.session_config.package().to_owned(),
568 self.session_config.lib_name().to_owned(),
569 imports,
570 );
571 self.worker.open_session(&config, cancellation, progress)
572 }
573
574 #[must_use]
576 pub fn dylib_path(&self) -> &Path {
577 &self.dylib_path
578 }
579
580 #[must_use]
582 pub fn session_config(&self) -> &LeanWorkerSessionConfig {
583 &self.session_config
584 }
585
586 #[must_use]
588 pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
589 self.validated_metadata.as_ref()
590 }
591
592 #[must_use]
594 pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
595 self.worker.runtime_metadata()
596 }
597
598 #[must_use]
600 pub fn worker(&self) -> &LeanWorker {
601 &self.worker
602 }
603
604 #[must_use]
606 pub fn worker_mut(&mut self) -> &mut LeanWorker {
607 &mut self.worker
608 }
609
610 pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
617 self.worker.terminate()
618 }
619}
620
621#[derive(Clone, Debug)]
622struct CapabilityMetadataCheck {
623 export: String,
624 request: Value,
625 expected: Option<LeanWorkerCapabilityMetadata>,
626}
627
628#[derive(Debug)]
629struct WorkerCapabilityArtifact {
630 dylib_path: PathBuf,
631 package: String,
632 module: String,
633}
634
635impl WorkerCapabilityArtifact {
636 fn from_built_capability(spec: &LeanBuiltCapability) -> Result<Self, LeanWorkerError> {
637 if let Ok(manifest_path) = spec.resolved_manifest_path() {
638 return Self::from_manifest(&manifest_path);
639 }
640
641 let dylib_path = spec.dylib_path().map_err(|err| LeanWorkerError::Setup {
642 message: err.to_string(),
643 })?;
644 let package = spec.package_name().ok_or_else(|| LeanWorkerError::Setup {
645 message: "LeanBuiltCapability is missing the Lake package name; call `.package(...)`".to_owned(),
646 })?;
647 let module = spec.module_name().ok_or_else(|| LeanWorkerError::Setup {
648 message: "LeanBuiltCapability is missing the root Lean module name; call `.module(...)`".to_owned(),
649 })?;
650 Ok(Self {
651 dylib_path,
652 package: package.to_owned(),
653 module: module.to_owned(),
654 })
655 }
656
657 fn from_manifest(manifest_path: &Path) -> Result<Self, LeanWorkerError> {
658 let bytes = std::fs::read(manifest_path).map_err(|err| LeanWorkerError::Bootstrap {
659 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
660 code: LeanLoaderDiagnosticCode::MissingManifest,
661 },
662 message: format!(
663 "could not read Lean capability manifest '{}': {err}",
664 manifest_path.display()
665 ),
666 })?;
667 let manifest: WorkerCapabilityManifest =
668 serde_json::from_slice(&bytes).map_err(|err| LeanWorkerError::Bootstrap {
669 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
670 code: LeanLoaderDiagnosticCode::MalformedManifest,
671 },
672 message: format!(
673 "Lean capability manifest '{}' is malformed: {err}",
674 manifest_path.display()
675 ),
676 })?;
677 if manifest.schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
678 return Err(LeanWorkerError::Bootstrap {
679 code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
680 code: LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
681 },
682 message: format!(
683 "unsupported Lean capability manifest schema {}; supported schema is {}",
684 manifest.schema_version,
685 lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
686 ),
687 });
688 }
689 Ok(Self {
690 dylib_path: manifest.primary_dylib,
691 package: manifest.package,
692 module: manifest.module,
693 })
694 }
695}
696
697#[derive(Deserialize)]
698struct WorkerCapabilityManifest {
699 schema_version: u64,
700 primary_dylib: PathBuf,
701 package: String,
702 module: String,
703}
704
705#[derive(Clone, Debug, Eq, PartialEq)]
712pub struct LeanWorkerChild {
713 executable_name: Option<String>,
714 explicit_path: Option<PathBuf>,
715 env_var: Option<String>,
716}
717
718impl LeanWorkerChild {
719 #[must_use]
722 pub fn sibling(executable_name: impl Into<String>) -> Self {
723 Self {
724 executable_name: Some(with_exe_suffix(executable_name.into())),
725 explicit_path: None,
726 env_var: None,
727 }
728 }
729
730 #[must_use]
732 pub fn path(path: impl Into<PathBuf>) -> Self {
733 Self {
734 executable_name: None,
735 explicit_path: Some(path.into()),
736 env_var: None,
737 }
738 }
739
740 #[must_use]
742 pub fn env_override(mut self, env_var: impl Into<String>) -> Self {
743 self.env_var = Some(env_var.into());
744 self
745 }
746
747 fn resolve(&self) -> Result<PathBuf, LeanWorkerError> {
748 let mut tried = Vec::new();
749 if let Some(env_var) = &self.env_var
750 && let Some(value) = env::var_os(env_var)
751 {
752 let path = PathBuf::from(value);
753 if path.is_file() {
754 return Ok(path);
755 }
756 tried.push(path);
757 return Err(LeanWorkerError::WorkerChildUnresolved { tried });
758 }
759 if let Some(path) = &self.explicit_path {
760 return Ok(path.clone());
761 }
762
763 let executable_name = self
764 .executable_name
765 .clone()
766 .unwrap_or_else(|| with_exe_suffix("lean-rs-worker-child".to_owned()));
767 tried.extend(candidate_sibling_worker_paths(&executable_name));
768 if executable_name == with_exe_suffix("lean-rs-worker-child".to_owned())
769 && let Some(path) = try_build_workspace_worker_child(&executable_name, &mut tried)
770 {
771 return Ok(path);
772 }
773 for path in dedup_paths(&tried) {
774 if path.is_file() {
775 return Ok(path);
776 }
777 }
778 Err(LeanWorkerError::WorkerChildUnresolved { tried })
779 }
780}
781
782impl Default for LeanWorkerChild {
783 fn default() -> Self {
784 Self::sibling("lean-rs-worker-child").env_override(WORKER_CHILD_ENV)
785 }
786}
787
788fn resolve_default_worker_executable() -> Result<PathBuf, LeanWorkerError> {
789 LeanWorkerChild::default().resolve()
790}
791
792fn validate_worker_child_path(path: &Path) -> Result<(), LeanWorkerError> {
793 if !path.is_file() {
794 return Err(LeanWorkerError::WorkerChildNotExecutable {
795 path: path.to_path_buf(),
796 reason: "path does not point to a file".to_owned(),
797 });
798 }
799 if !is_executable_file(path) {
800 return Err(LeanWorkerError::WorkerChildNotExecutable {
801 path: path.to_path_buf(),
802 reason: "file is not executable by this user".to_owned(),
803 });
804 }
805 Ok(())
806}
807
808#[cfg(unix)]
809fn is_executable_file(path: &Path) -> bool {
810 use std::os::unix::fs::PermissionsExt as _;
811
812 std::fs::metadata(path).is_ok_and(|metadata| metadata.permissions().mode() & 0o111 != 0)
813}
814
815#[cfg(not(unix))]
816fn is_executable_file(_path: &Path) -> bool {
817 true
818}
819
820fn check_from_open_error(err: &LeanWorkerError) -> LeanWorkerBootstrapCheck {
821 match err {
822 LeanWorkerError::WorkerChildUnresolved { tried } => LeanWorkerBootstrapCheck::error(
823 LeanWorkerBootstrapDiagnosticCode::WorkerChildUnresolved,
824 "worker child",
825 format!("could not resolve worker child; tried {}", format_paths(tried)),
826 "ship an app-owned worker child binary beside the app or configure LeanWorkerChild::env_override",
827 ),
828 LeanWorkerError::WorkerChildNotExecutable { path, reason } => LeanWorkerBootstrapCheck::error(
829 LeanWorkerBootstrapDiagnosticCode::WorkerChildNotExecutable,
830 path.display().to_string(),
831 reason.clone(),
832 "ship an app-owned worker child binary and ensure it is executable",
833 ),
834 LeanWorkerError::Bootstrap { code, message } => LeanWorkerBootstrapCheck::error(
835 *code,
836 code.as_str(),
837 message.clone(),
838 "fix the reported bootstrap input",
839 ),
840 LeanWorkerError::Handshake { message } => LeanWorkerBootstrapCheck::error(
841 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
842 "worker handshake",
843 message.clone(),
844 "ensure the worker child calls lean_rs_worker::run_worker_child_stdio and matches this crate version",
845 ),
846 LeanWorkerError::Timeout {
847 operation: "startup", ..
848 } => LeanWorkerBootstrapCheck::error(
849 LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
850 "worker handshake",
851 err.to_string(),
852 "check that the worker child starts promptly and writes the lean-rs-worker handshake",
853 ),
854 LeanWorkerError::CapabilityMetadataMismatch { export, .. } => LeanWorkerBootstrapCheck::error(
855 LeanWorkerBootstrapDiagnosticCode::CapabilityMetadataMismatch,
856 export.clone(),
857 "capability metadata did not match the requested expectation",
858 "rebuild or select a capability whose metadata matches the caller expectation",
859 ),
860 other @ (LeanWorkerError::Spawn { .. }
861 | LeanWorkerError::CapabilityBuild { .. }
862 | LeanWorkerError::Setup { .. }
863 | LeanWorkerError::Protocol { .. }
864 | LeanWorkerError::Worker { .. }
865 | LeanWorkerError::ChildExited { .. }
866 | LeanWorkerError::ChildPanicOrAbort { .. }
867 | LeanWorkerError::Timeout { .. }
868 | LeanWorkerError::Cancelled { .. }
869 | LeanWorkerError::ProgressPanic { .. }
870 | LeanWorkerError::DataSinkPanic { .. }
871 | LeanWorkerError::DiagnosticSinkPanic { .. }
872 | LeanWorkerError::StreamExportFailed { .. }
873 | LeanWorkerError::StreamCallbackFailed { .. }
874 | LeanWorkerError::StreamRowMalformed { .. }
875 | LeanWorkerError::CapabilityMetadataMalformed { .. }
876 | LeanWorkerError::CapabilityDoctorMalformed { .. }
877 | LeanWorkerError::TypedCommandRequestEncode { .. }
878 | LeanWorkerError::TypedCommandResponseDecode { .. }
879 | LeanWorkerError::TypedCommandRowDecode { .. }
880 | LeanWorkerError::TypedCommandSummaryDecode { .. }
881 | LeanWorkerError::LeaseInvalidated { .. }
882 | LeanWorkerError::WorkerPoolExhausted { .. }
883 | LeanWorkerError::WorkerPoolMemoryBudgetExceeded { .. }
884 | LeanWorkerError::WorkerPoolQueueTimeout { .. }
885 | LeanWorkerError::UnsupportedRequest { .. }
886 | LeanWorkerError::Wait { .. }) => LeanWorkerBootstrapCheck::error(
887 LeanWorkerBootstrapDiagnosticCode::WorkerStartupFailed,
888 "worker bootstrap",
889 other.to_string(),
890 "run the bootstrap check in a deployment environment and rebuild the worker child or capability artifact",
891 ),
892 }
893}
894
895fn format_paths(paths: &[PathBuf]) -> String {
896 if paths.is_empty() {
897 return "<none>".to_owned();
898 }
899 paths
900 .iter()
901 .map(|path| path.display().to_string())
902 .collect::<Vec<_>>()
903 .join(", ")
904}
905
906fn bound_bootstrap_text(mut text: String) -> String {
907 const LIMIT: usize = 1_024;
908 if text.len() <= LIMIT {
909 return text;
910 }
911 while !text.is_char_boundary(LIMIT) {
912 text.pop();
913 }
914 text.truncate(LIMIT);
915 text.push_str("...");
916 text
917}
918
919fn candidate_sibling_worker_paths(executable_name: &str) -> Vec<PathBuf> {
920 let mut tried = Vec::new();
921 if let Ok(current_exe) = env::current_exe() {
922 if let Some(dir) = current_exe.parent() {
923 tried.push(dir.join(executable_name));
924 }
925 if let Some(profile_dir) = current_exe.parent().and_then(Path::parent) {
926 tried.push(profile_dir.join(executable_name));
927 }
928 }
929 tried
930}
931
932fn with_exe_suffix(mut executable_name: String) -> String {
933 if !env::consts::EXE_SUFFIX.is_empty() && !executable_name.ends_with(env::consts::EXE_SUFFIX) {
934 executable_name.push_str(env::consts::EXE_SUFFIX);
935 }
936 executable_name
937}
938
939fn infer_lake_project_root_from_dylib(dylib_path: &Path) -> Result<PathBuf, LeanWorkerError> {
940 let lib_dir = dylib_path.parent();
941 let build_dir = lib_dir.and_then(Path::parent);
942 let lake_dir = build_dir.and_then(Path::parent);
943 let project_root = lake_dir.and_then(Path::parent);
944 match (lib_dir, build_dir, lake_dir, project_root) {
945 (Some(lib), Some(build), Some(lake), Some(root))
946 if lib.file_name().is_some_and(|name| name == "lib")
947 && build.file_name().is_some_and(|name| name == "build")
948 && lake.file_name().is_some_and(|name| name == ".lake") =>
949 {
950 Ok(root.to_path_buf())
951 }
952 _ => Err(LeanWorkerError::Setup {
953 message: format!(
954 "built capability dylib '{}' is not under a standard .lake/build/lib directory",
955 dylib_path.display()
956 ),
957 }),
958 }
959}
960
961fn try_build_workspace_worker_child(executable_name: &str, tried: &mut Vec<PathBuf>) -> Option<PathBuf> {
962 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
963 let workspace = manifest_dir.parent()?.parent()?;
964 if !workspace
965 .join("crates")
966 .join("lean-rs-worker")
967 .join("Cargo.toml")
968 .is_file()
969 {
970 return None;
971 }
972
973 let debug = workspace.join("target").join("debug").join(executable_name);
974 let release = workspace.join("target").join("release").join(executable_name);
975 tried.push(debug.clone());
976 tried.push(release.clone());
977 if debug.is_file() {
978 return Some(debug);
979 }
980 if release.is_file() {
981 return Some(release);
982 }
983
984 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
985 let status = Command::new(cargo)
986 .current_dir(workspace)
987 .args(["build", "-p", "lean-rs-worker", "--bin", "lean-rs-worker-child"])
988 .status()
989 .ok()?;
990 if !status.success() {
991 return None;
992 }
993 debug.is_file().then_some(debug)
994}
995
996fn dedup_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
997 let mut unique = Vec::new();
998 for path in paths {
999 if !unique.iter().any(|existing| existing == path) {
1000 unique.push(path.clone());
1001 }
1002 }
1003 unique
1004}