Skip to main content

lean_rs_worker_parent/
capability.rs

1//! Builders for worker-backed downstream capabilities and host sessions.
2//!
3//! This module composes worker child resolution, worker startup, and session
4//! opening. User-export capabilities also build a Lake shared-library
5//! target and may validate downstream metadata. Shim-backed host sessions skip
6//! that user dylib path entirely and use only the bundled host services.
7
8use 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/// Builder for a worker-backed Lean capability session.
43///
44/// The builder hides the common setup sequence for downstream tools:
45///
46/// 1. build the Lake shared-library target with `lean-toolchain`;
47/// 2. resolve and start the `lean-rs-worker-child` process;
48/// 3. health-check the worker;
49/// 4. open the configured host session once; and
50/// 5. optionally validate downstream capability metadata.
51///
52/// Callers still provide the Lake project root, package name, library target,
53/// and imports because those are the downstream capability's identity. Worker
54/// framing, child lifecycle, path probing, timeouts, and restart policy stay
55/// behind the builder.
56///
57/// Use [`LeanWorkerHostHandleBuilder::shims_only`] for tools that only need
58/// the bundled Meta, elaboration, kernel, declaration, and info-tree services.
59/// That path does not build or load the user's `:shared` facet and therefore
60/// keeps working when unrelated user modules break the shared library build.
61#[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    /// Create a builder for a capability Lake project and library.
86    ///
87    /// `project_root` is the capability project's directory containing
88    /// `lakefile.lean`; it owns the dylib and manifest this builder builds or
89    /// loads. `package` is the Lake package name used by `lean-rs-host`, and
90    /// `lib_name` is the Lake `lean_lib` target to build and load. Session
91    /// imports default to this same project unless
92    /// [`Self::import_workspace_root`] sets a separate target workspace.
93    #[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    /// Create a builder from a build-script produced capability.
124    ///
125    /// Manifest-backed descriptors are the canonical packaged-app path. The
126    /// builder reads package, module, and primary dylib facts from the
127    /// manifest, then infers the capability Lake project root from the
128    /// standard `.lake/build/lib/<dylib>` layout. Session imports default to
129    /// that inferred project unless [`Self::import_workspace_root`] sets a
130    /// separate target workspace. Direct dylib descriptors remain supported as
131    /// a compatibility path when callers also provide package and module names.
132    ///
133    /// # Errors
134    ///
135    /// Returns `LeanWorkerError` if manifest data cannot be parsed, the
136    /// fallback dylib path cannot be resolved, the compatibility descriptor is
137    /// missing package/module names, or the dylib is not under a standard Lake
138    /// build directory.
139    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    /// Use an explicit `lean-rs-worker-child` executable.
169    ///
170    /// Tests and packaged applications should use this when the worker child
171    /// is not discoverable beside the current executable.
172    #[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    /// Resolve the worker executable with a packaged worker-child locator.
179    #[must_use]
180    pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
181        self.worker_child = Some(child);
182        self
183    }
184
185    /// Use a separate target Lake workspace root for session imports.
186    ///
187    /// The capability dylib and manifest still come from this builder's
188    /// capability project. This root is the single target workspace whose own
189    /// `.lake/build/lib/lean` entry and `lake-manifest.json` dependency closure
190    /// the worker session imports against. It is not merged with the
191    /// capability project's search path.
192    ///
193    /// Tools whose capability project and audited workspace are distinct must
194    /// set this explicitly. Otherwise the session imports against the
195    /// capability project, preserving the legacy single-project behavior.
196    ///
197    /// Capability exports that import modules must rely on the host-installed
198    /// search path. They must not call `Lean.initSearchPath` or rebuild the
199    /// search path from `LEAN_PATH`, because doing so resets Lean's search path
200    /// and discards this target workspace root.
201    #[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    /// Select the full-session import profile used for worker host sessions.
208    #[must_use]
209    pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
210        self.import_profile = profile;
211        self
212    }
213
214    /// Set the maximum time to wait for worker startup.
215    #[must_use]
216    pub fn startup_timeout(mut self, timeout: Duration) -> Self {
217        self.startup_timeout = Some(timeout);
218        self
219    }
220
221    /// Set the maximum time to wait for one worker request.
222    #[must_use]
223    pub fn request_timeout(mut self, timeout: Duration) -> Self {
224        self.request_timeout = Some(timeout);
225        self
226    }
227
228    /// Set the maximum time to wait for graceful worker shutdown.
229    #[must_use]
230    pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
231        self.shutdown_timeout = Some(timeout);
232        self
233    }
234
235    /// Use the documented long-running request timeout profile.
236    #[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    /// Set the worker restart policy used after startup.
243    #[must_use]
244    pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
245        self.restart_policy = Some(policy);
246        self
247    }
248
249    /// Configure the parent-side hard RSS kill watchdog for in-flight worker
250    /// requests.
251    #[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    /// Set typed limits for the worker child's module snapshot cache.
258    ///
259    /// These are deliberately not exposed as a generic child-env passthrough:
260    /// the cache knobs are part of the worker lifecycle contract, and callers
261    /// should not need to know the child process's environment-variable names.
262    #[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    /// Set the per-frame byte cap negotiated with the worker child at handshake.
269    ///
270    /// See [`LeanWorkerConfig::max_frame_bytes`] for the policy and the
271    /// `[MIN_FRAME_BYTES, MAX_FRAME_BYTES_HARD_CAP]` clamp. Raise this for
272    /// capabilities whose single logical result composes into one frame
273    /// (e.g. an outline of an entire module, a file-scoped diagnostics
274    /// snapshot) and would otherwise trip `FrameTooLarge`.
275    #[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    /// Validate generic capability metadata after the session opens.
282    ///
283    /// The export must have ABI `String -> IO String`, matching
284    /// `LeanWorkerSession::capability_metadata`. The returned metadata is
285    /// stored on the opened capability for callers that need it.
286    #[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    /// Validate that a capability metadata export returns the expected facts.
299    ///
300    /// This is the pool-facing metadata expectation hook. The metadata remains
301    /// downstream-defined; `lean-rs-worker` only checks that the generic
302    /// metadata envelope matches the caller's requested expectation.
303    #[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    /// Trust one manifest-backed metadata export with ABI `String -> IO String`.
321    #[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    /// Trust one manifest-backed doctor export with ABI `String -> IO String`.
328    #[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    /// Trust one manifest-backed JSON command export with ABI `String -> IO String`.
335    #[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    /// Trust one manifest-backed streaming command export with ABI `String, USize, USize -> IO UInt8`.
342    #[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    /// Return the session reuse key represented by this builder.
359    ///
360    /// The key is for worker-pool reuse only. It is not a downstream cache key
361    /// and does not encode row schemas, ranking, reporting, or source
362    /// provenance.
363    #[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    /// Check deployment facts before running a real worker command.
400    ///
401    /// The report validates the worker child locator, manifest-backed
402    /// capability artifact when present, worker protocol handshake, session
403    /// opening, and optional metadata expectation. It keeps child paths,
404    /// protocol frames, and loader environment details below the worker
405    /// boundary.
406    #[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    /// Build the Lake target, start the worker, open the session, and return a ready capability.
443    ///
444    /// # Errors
445    ///
446    /// Returns `LeanWorkerError` if Lake cannot build the target, the worker
447    /// child cannot be resolved or spawned, the worker fails startup/health,
448    /// the session cannot open, or metadata validation fails.
449    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/// Builder for a worker-backed host session that loads only bundled shims.
541///
542/// This is the bootstrap path for tools that use the standard host services
543/// exposed through `lean-rs-host`: Meta queries, elaboration, kernel checking,
544/// declaration listing, source ranges, and info trees. It deliberately has no
545/// package/library fields and no metadata validation hook because no user
546/// `@[export]` dylib is built or opened.
547#[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/// Typed limits for the worker child's module snapshot cache.
563///
564/// The worker child still receives these values as environment variables at
565/// launch time, but the public API names the lifecycle policy rather than the
566/// transport mechanism. A field left unset uses the child default.
567#[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    /// Set the maximum retained cache entries.
578    #[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    /// Set the time-to-live for retained module snapshots.
585    #[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    /// Set the approximate retained-cache byte ceiling.
592    #[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    /// Set the child RSS guard above which the child clears retained snapshots
599    /// before cacheable module-query requests.
600    #[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    /// Set the child RSS ceiling at or above which a non-positive
607    /// `verify_declaration` verdict (e.g. `NotFound`) is relabeled to
608    /// `BudgetExceeded`: near the cap the worker cannot distinguish a genuine
609    /// "name absent" from an elaboration silently degraded by memory pressure.
610    /// Leave unset (the default) to disable the taint; set it well above the
611    /// warm mathlib baseline so genuine name-absent queries are not mislabeled.
612    #[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    /// Create a shims-only worker host-session builder for a Lake project.
621    ///
622    /// `project_root` is the directory containing `lakefile.lean`. `imports`
623    /// are the modules to import when the builder performs its initial session
624    /// open. The builder does not build a Lake `:shared` target.
625    #[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    /// Use an explicit `lean-rs-worker-child` executable.
643    #[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    /// Select the full-session import profile used for opened host sessions.
650    #[must_use]
651    pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
652        self.import_profile = profile;
653        self
654    }
655
656    /// Resolve the worker executable with a packaged worker-child locator.
657    #[must_use]
658    pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
659        self.worker_child = Some(child);
660        self
661    }
662
663    /// Set the maximum time to wait for worker startup.
664    #[must_use]
665    pub fn startup_timeout(mut self, timeout: Duration) -> Self {
666        self.startup_timeout = Some(timeout);
667        self
668    }
669
670    /// Set the maximum time to wait for one worker request.
671    #[must_use]
672    pub fn request_timeout(mut self, timeout: Duration) -> Self {
673        self.request_timeout = Some(timeout);
674        self
675    }
676
677    /// Set the maximum time to wait for graceful worker shutdown.
678    #[must_use]
679    pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
680        self.shutdown_timeout = Some(timeout);
681        self
682    }
683
684    /// Use the documented long-running request timeout profile.
685    #[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    /// Set the worker restart policy used after startup.
692    #[must_use]
693    pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
694        self.restart_policy = Some(policy);
695        self
696    }
697
698    /// Configure the parent-side hard RSS kill watchdog for in-flight worker
699    /// requests.
700    #[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    /// Set typed limits for the worker child's module snapshot cache.
707    ///
708    /// These are applied when the worker child is spawned. They are scoped to
709    /// this handle and do not mutate process-global environment variables.
710    #[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    /// Set the per-frame byte cap negotiated with the worker child at handshake.
717    #[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    /// Check worker bootstrap facts before running a real command.
724    ///
725    /// The report validates the worker child locator, protocol handshake, and
726    /// shims-only session opening. It never builds a user shared-library target.
727    #[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    /// Start the worker, open a shims-only host session once, and return a ready handle.
744    ///
745    /// # Errors
746    ///
747    /// Returns `LeanWorkerError` if the worker child cannot be resolved or
748    /// spawned, startup/health fails, or the shims-only session cannot open.
749    /// This method does not build a user Lake shared-library target.
750    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/// Stable worker bootstrap diagnostic codes.
790#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
791pub enum LeanWorkerBootstrapDiagnosticCode {
792    /// The worker child locator did not resolve to a file.
793    WorkerChildUnresolved,
794    /// The worker child exists but is not executable.
795    WorkerChildNotExecutable,
796    /// Manifest-backed capability preflight reported a loader/artifact issue.
797    CapabilityPreflight { code: LeanLoaderDiagnosticCode },
798    /// The worker child did not complete the protocol handshake.
799    WorkerHandshakeFailed,
800    /// Capability metadata did not match the caller's expectation.
801    CapabilityMetadataMismatch,
802    /// Worker bootstrap failed for a reason outside the named deployment checks.
803    WorkerStartupFailed,
804}
805
806impl LeanWorkerBootstrapDiagnosticCode {
807    /// Stable string identifier suitable for logs and support reports.
808    #[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/// Severity of one worker bootstrap finding.
828#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
829pub enum LeanWorkerBootstrapSeverity {
830    /// Informational finding that does not block startup.
831    Info,
832    /// Suspicious state that may still start.
833    Warning,
834    /// The worker should not start real commands until this is fixed.
835    Error,
836}
837
838/// One bounded worker bootstrap finding.
839#[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    /// Stable diagnostic code.
865    #[must_use]
866    pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
867        self.code
868    }
869
870    /// Whether this finding blocks worker startup.
871    #[must_use]
872    pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
873        self.severity
874    }
875
876    /// Child binary, artifact, export, or protocol step this finding concerns.
877    #[must_use]
878    pub fn subject(&self) -> &str {
879        &self.subject
880    }
881
882    /// Bounded explanation of the finding.
883    #[must_use]
884    pub fn message(&self) -> &str {
885        &self.message
886    }
887
888    /// Bounded repair hint for packaged applications.
889    #[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/// Structured result of worker bootstrap checks for one capability builder.
900#[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    /// All bootstrap findings.
911    #[must_use]
912    pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
913        &self.checks
914    }
915
916    /// Blocking bootstrap findings.
917    pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
918        self.checks
919            .iter()
920            .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
921    }
922
923    /// Whether the worker bootstrap checks found no blocking findings.
924    #[must_use]
925    pub fn is_ok(&self) -> bool {
926        self.first_error().is_none()
927    }
928
929    /// First blocking finding, if any.
930    #[must_use]
931    pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
932        self.errors().next()
933    }
934}
935
936/// A worker-backed capability with its Lake target built and worker started.
937///
938/// The value owns the worker supervisor and the session configuration. It is
939/// the normal entry point for downstream capability use until the typed command
940/// facade lands on top of it.
941#[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    /// Open a worker session for this capability.
951    ///
952    /// The builder has already proved that the session can open. This method
953    /// is still fallible because worker cycling, cancellation, or a child
954    /// failure may require a fresh session.
955    ///
956    /// # Errors
957    ///
958    /// Returns `LeanWorkerError` if the worker is dead, the child cannot open
959    /// the configured imports, cancellation is already requested, a progress
960    /// sink panics, or protocol communication fails.
961    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    /// Open a worker session with a caller-supplied import set, overriding the imports
974    /// the builder was constructed with. The capability's `project_root` / `package` /
975    /// `lib_name` are unchanged.
976    ///
977    /// Lifecycle is identical to [`open_session`](Self::open_session): the returned
978    /// session borrows from `&mut self` and dies when dropped.
979    ///
980    /// # Errors
981    ///
982    /// Same as [`open_session`](Self::open_session).
983    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    /// Return the built capability dylib path resolved by `lean-toolchain`.
994    #[must_use]
995    pub fn dylib_path(&self) -> &Path {
996        &self.dylib_path
997    }
998
999    /// Return the session configuration used by this capability.
1000    #[must_use]
1001    pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1002        &self.session_config
1003    }
1004
1005    /// Return capability metadata validated by the builder, if requested.
1006    #[must_use]
1007    pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
1008        self.validated_metadata.as_ref()
1009    }
1010
1011    /// Return protocol/runtime facts captured from the worker handshake.
1012    #[must_use]
1013    pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1014        self.worker.runtime_metadata()
1015    }
1016
1017    /// Return a snapshot of worker lifecycle counters.
1018    #[must_use]
1019    pub fn stats(&self) -> LeanWorkerStats {
1020        self.worker.stats()
1021    }
1022
1023    /// Return policy-facing lifecycle facts for this worker.
1024    #[must_use]
1025    pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1026        self.worker.lifecycle_snapshot()
1027    }
1028
1029    /// Return the current worker lifecycle status.
1030    ///
1031    /// # Errors
1032    ///
1033    /// Returns `LeanWorkerError` if checking the process status fails.
1034    pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1035        self.worker.status()
1036    }
1037
1038    /// Measure the current child RSS in KiB when supported by the platform.
1039    pub fn rss_kib(&mut self) -> Option<u64> {
1040        self.worker.rss_kib()
1041    }
1042
1043    /// Explicitly cycle the worker process.
1044    ///
1045    /// # Errors
1046    ///
1047    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1048    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    /// Set the request timeout for subsequent commands.
1061    pub fn set_request_timeout(&mut self, timeout: Duration) {
1062        self.worker.set_request_timeout(timeout);
1063    }
1064
1065    #[doc(hidden)]
1066    /// Kill the child process for supervisor tests.
1067    ///
1068    /// # Errors
1069    ///
1070    /// Returns `LeanWorkerError` if the worker is already dead or kill fails.
1071    pub fn __kill_for_test(&mut self) -> Result<(), LeanWorkerError> {
1072        self.worker.__kill_for_test()
1073    }
1074
1075    /// Terminate the worker child and return its exit status.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Returns `LeanWorkerError` if the worker is already dead, the terminate
1080    /// request fails, or waiting for the child fails.
1081    #[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    /// Shut down the worker child and return structured shutdown status.
1087    ///
1088    /// # Errors
1089    ///
1090    /// Returns `LeanWorkerError` if shutdown escalation or wait/reap fails.
1091    pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1092        self.worker.shutdown()
1093    }
1094}
1095
1096/// A worker-backed host session handle that is backed only by bundled shims.
1097///
1098/// Unlike [`LeanWorkerCapability`], this type has no user dylib path and no
1099/// metadata exports. It owns the worker supervisor and a shims-only session
1100/// configuration; each opened session can import project `.olean` files and
1101/// call the standard worker services.
1102#[derive(Debug)]
1103pub struct LeanWorkerHostHandle {
1104    worker: LeanWorker,
1105    session_config: LeanWorkerSessionConfig,
1106}
1107
1108impl LeanWorkerHostHandle {
1109    /// Open a worker session for this host handle.
1110    ///
1111    /// The builder has already proved that the session can open. This method
1112    /// is still fallible because worker cycling, cancellation, or a child
1113    /// failure may require a fresh session.
1114    ///
1115    /// # Errors
1116    ///
1117    /// Returns `LeanWorkerError` if the worker is dead, the child cannot open
1118    /// the configured imports, cancellation is already requested, a progress
1119    /// sink panics, or protocol communication fails.
1120    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    /// Open a worker session with a caller-supplied import set, overriding the
1129    /// imports the builder was constructed with.
1130    ///
1131    /// Lifecycle is identical to [`open_session`](Self::open_session): the
1132    /// returned session borrows from `&mut self` and dies when dropped.
1133    ///
1134    /// # Errors
1135    ///
1136    /// Same as [`open_session`](Self::open_session).
1137    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    /// Open a session with `imports`, process one module query, and retry once
1169    /// if an automatic worker lifecycle cycle invalidated the just-opened
1170    /// session before the command frame was sent.
1171    ///
1172    /// # Errors
1173    ///
1174    /// Returns `LeanWorkerError` for worker, protocol, cancellation, or
1175    /// progress-sink failures other than the internally retried
1176    /// `session_missing` race.
1177    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    /// Open a session with `imports`, process one module-query batch, and
1192    /// retry once on the `session_missing` lifecycle race.
1193    ///
1194    /// # Errors
1195    ///
1196    /// Same as [`Self::process_module_query_with_imports`].
1197    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    /// Open a session with `imports` and inspect one declaration.
1213    ///
1214    /// # Errors
1215    ///
1216    /// Same as [`Self::process_module_query_with_imports`].
1217    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    /// Open a session with `imports` and run bounded declaration search.
1230    ///
1231    /// # Errors
1232    ///
1233    /// Same as [`Self::process_module_query_with_imports`].
1234    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    /// Open a session with `imports` and try proof fragments in-memory.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Same as [`Self::process_module_query_with_imports`].
1251    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    /// Open a session with `imports` and verify one declaration in-memory.
1265    ///
1266    /// # Errors
1267    ///
1268    /// Same as [`Self::process_module_query_with_imports`].
1269    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    /// Open a session with `imports` and verify several declarations in one
1283    /// in-memory source snapshot.
1284    ///
1285    /// # Errors
1286    ///
1287    /// Same as [`Self::process_module_query_with_imports`].
1288    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    /// Return the session configuration used by this host handle.
1302    #[must_use]
1303    pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1304        &self.session_config
1305    }
1306
1307    /// Return protocol/runtime facts captured from the worker handshake.
1308    #[must_use]
1309    pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1310        self.worker.runtime_metadata()
1311    }
1312
1313    /// Return a snapshot of worker lifecycle counters.
1314    #[must_use]
1315    pub fn stats(&self) -> LeanWorkerStats {
1316        self.worker.stats()
1317    }
1318
1319    /// Return policy-facing lifecycle facts for this worker.
1320    #[must_use]
1321    pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1322        self.worker.lifecycle_snapshot()
1323    }
1324
1325    /// Return the current worker lifecycle status.
1326    ///
1327    /// # Errors
1328    ///
1329    /// Returns `LeanWorkerError` if checking the process status fails.
1330    pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1331        self.worker.status()
1332    }
1333
1334    /// Measure the current child RSS in KiB when supported by the platform.
1335    pub fn rss_kib(&mut self) -> Option<u64> {
1336        self.worker.rss_kib()
1337    }
1338
1339    /// Explicitly cycle the worker process.
1340    ///
1341    /// # Errors
1342    ///
1343    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1344    pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1345        self.worker.cycle()
1346    }
1347
1348    /// Restart this worker using its original configuration.
1349    ///
1350    /// # Errors
1351    ///
1352    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1353    pub fn restart(&mut self) -> Result<(), LeanWorkerError> {
1354        self.worker.restart()
1355    }
1356
1357    /// Terminate the worker child and return its exit status.
1358    ///
1359    /// # Errors
1360    ///
1361    /// Returns `LeanWorkerError` if the worker is already dead, the terminate
1362    /// request fails, or waiting for the child fails.
1363    #[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    /// Shut down the worker child and return structured shutdown status.
1369    ///
1370    /// # Errors
1371    ///
1372    /// Returns `LeanWorkerError` if shutdown escalation or wait/reap fails.
1373    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/// Locator for an app-owned worker child executable.
1547///
1548/// Dependency binaries are not automatically installed with downstream
1549/// applications. Production apps should ship a tiny binary that calls
1550/// `lean_rs_worker_child::run_worker_child_stdio` and point the capability
1551/// builder at it through this locator.
1552///
1553/// # Toolchain binding
1554///
1555/// A worker child binary is *built against one Lean toolchain*: its rpath
1556/// points at one `libleanshared`, and `LEAN_SYSROOT` at spawn time must point
1557/// at the matching stdlib oleans (`<sysroot>/lib/lean/Init.olean`). Mismatched
1558/// rpath and sysroot abort with `incompatible header` before the handshake.
1559///
1560/// The locator carries both: the binary path (via [`Self::path`] or
1561/// [`Self::sibling`]) and, optionally, the matching sysroot (via
1562/// [`Self::for_toolchain`] or [`Self::lean_sysroot`]). When the supervisor
1563/// spawns the child, it sets `LEAN_SYSROOT` from the locator (or from
1564/// [`lean_toolchain::discover_toolchain`] as a fallback) so callers never have
1565/// to thread the env var manually.
1566///
1567/// # Design note: no generic `env(key, value)` passthrough
1568///
1569/// `LeanWorkerCapabilityBuilder` and `LeanWorkerChild` deliberately do **not**
1570/// expose a general `env(key, value)` builder. Every environment variable the
1571/// worker child cares about has a typed method whose name describes the
1572/// invariant it enforces (e.g. [`Self::lean_sysroot`] enforces the
1573/// rpath/sysroot match). If a future env var needs to be plumbed through, add
1574/// a typed builder for it—do **not** add a generic `env(...)`. Generic
1575/// passthroughs leak implementation knowledge (env var names, framing
1576/// invariants) into every caller and erode the structural guarantee that
1577/// supported configurations cannot be misconstructed.
1578///
1579/// # Provisioning is the caller's job
1580///
1581/// This locator only *finds* a worker child; it does not install one.
1582/// Dependency binaries are not shipped with downstream applications, so a
1583/// downstream must provision its own worker child — ship it beside the host
1584/// binary (resolved by [`Self::sibling`]), point at it explicitly
1585/// ([`Self::path`] / [`Self::for_toolchain`]), or supply it through an
1586/// [`Self::env_override`] variable. The one exception is a developer
1587/// convenience internal to the `lean-rs` source tree: when the requested name
1588/// is the default `lean-rs-worker-child` *and* resolution runs from within the
1589/// `lean-rs` workspace, [`Self::sibling`] also falls back to building that
1590/// binary with `cargo build -p lean-rs-worker-child` (so the workspace's own
1591/// examples and profiling harness run with no extra steps). That fallback is
1592/// keyed to `lean-rs`'s own `CARGO_MANIFEST_DIR` and is **inert for every
1593/// downstream** — outside the source tree it finds no workspace to build and
1594/// no-ops. Do not rely on it from a consuming crate.
1595#[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    /// Locate a worker child beside the current executable, or beside the
1605    /// Cargo profile directory during tests and `cargo run`.
1606    ///
1607    /// For the default name `lean-rs-worker-child`, resolution additionally
1608    /// falls back to building the binary from source — but only inside the
1609    /// `lean-rs` workspace itself. See the [type-level provisioning
1610    /// note](Self#provisioning-is-the-callers-job): that fallback is an
1611    /// in-tree developer convenience and is inert for downstream callers, which
1612    /// must ship or point at their own worker child.
1613    #[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    /// Use an explicit worker child path.
1624    #[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    /// Locate a worker child and declare the Lean toolchain its rpath was
1635    /// built against.
1636    ///
1637    /// `sysroot` is the Lean prefix containing `lib/lean/Init.olean`. The
1638    /// supervisor sets `LEAN_SYSROOT` to this value when spawning the child,
1639    /// so a single parent process can host multiple workers each pinned to a
1640    /// different toolchain.
1641    #[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    /// Set or override the Lean sysroot the spawned child uses.
1652    ///
1653    /// When unset, the supervisor falls back to
1654    /// [`lean_toolchain::discover_toolchain`] at spawn time.
1655    #[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    /// Add an environment-variable override for launchers and tests.
1662    #[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    /// Return the sysroot the supervisor will set as `LEAN_SYSROOT`.
1669    ///
1670    /// Returns the explicit sysroot if one was bound via
1671    /// [`Self::for_toolchain`] or [`Self::lean_sysroot`]; otherwise runs
1672    /// [`lean_toolchain::discover_toolchain`] with default options and returns
1673    /// the discovered prefix.
1674    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        // In-tree convenience only: `try_build_workspace_worker_child` can build
1708        // exactly the `lean-rs-worker-child` binary and only when run from the
1709        // `lean-rs` workspace, so it is gated on that name (a differently-named
1710        // sibling would otherwise build the wrong binary) and no-ops downstream.
1711        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
1913/// Build `lean-rs-worker-child` from source as an in-tree developer
1914/// convenience for the workspace's own examples, tests, and profiling harness.
1915///
1916/// `CARGO_MANIFEST_DIR` is baked at compile time of this crate, so the derived
1917/// `workspace` only contains `crates/lean-rs-worker-child/Cargo.toml` when this
1918/// crate is being built *as part of the `lean-rs` source tree*. For a
1919/// downstream that depends on the published crate, `CARGO_MANIFEST_DIR` points
1920/// into the registry cache, the guard below fails, and this returns `None`
1921/// without side effects. Downstreams must therefore provision their own worker
1922/// child (see [`LeanWorkerChild`]).
1923fn 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        // The supervisor only sets `LEAN_SYSROOT`; it does not validate that
2127        // the path exists. Validation is the spawned child's responsibility
2128        // (an invalid sysroot manifests as a typed handshake/abort error
2129        // carrying the child's bootstrap stderr).
2130        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}