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    num_threads: Option<u32>,
80    lean_max_memory_kib: Option<u64>,
81    metadata_check: Option<CapabilityMetadataCheck>,
82    max_frame_bytes: Option<u32>,
83    worker_export_signatures: Vec<LeanExportSignature>,
84}
85
86impl LeanWorkerCapabilityBuilder {
87    /// Create a builder for a capability Lake project and library.
88    ///
89    /// `project_root` is the capability project's directory containing
90    /// `lakefile.lean`; it owns the dylib and manifest this builder builds or
91    /// loads. `package` is the Lake package name used by `lean-rs-host`, and
92    /// `lib_name` is the Lake `lean_lib` target to build and load. Session
93    /// imports default to this same project unless
94    /// [`Self::import_workspace_root`] sets a separate target workspace.
95    #[must_use]
96    pub fn new(
97        project_root: impl Into<PathBuf>,
98        package: impl Into<String>,
99        lib_name: impl Into<String>,
100        imports: impl IntoIterator<Item = impl Into<String>>,
101    ) -> Self {
102        Self {
103            project_root: project_root.into(),
104            import_workspace_root: None,
105            package: package.into(),
106            lib_name: lib_name.into(),
107            imports: imports.into_iter().map(Into::into).collect(),
108            import_profile: LeanWorkerSessionImportProfile::default(),
109            built_dylib_path: None,
110            built_manifest_path: None,
111            built_capability: None,
112            worker_child: None,
113            startup_timeout: None,
114            request_timeout: None,
115            shutdown_timeout: None,
116            restart_policy: None,
117            rss_hard_limit: None,
118            module_cache_limits: None,
119            num_threads: None,
120            lean_max_memory_kib: None,
121            metadata_check: None,
122            max_frame_bytes: None,
123            worker_export_signatures: Vec::new(),
124        }
125    }
126
127    /// Create a builder from a build-script produced capability.
128    ///
129    /// Manifest-backed descriptors are the canonical packaged-app path. The
130    /// builder reads package, module, and primary dylib facts from the
131    /// manifest, then infers the capability Lake project root from the
132    /// standard `.lake/build/lib/<dylib>` layout. Session imports default to
133    /// that inferred project unless [`Self::import_workspace_root`] sets a
134    /// separate target workspace. Direct dylib descriptors remain supported as
135    /// a compatibility path when callers also provide package and module names.
136    ///
137    /// # Errors
138    ///
139    /// Returns `LeanWorkerError` if manifest data cannot be parsed, the
140    /// fallback dylib path cannot be resolved, the compatibility descriptor is
141    /// missing package/module names, or the dylib is not under a standard Lake
142    /// build directory.
143    pub fn from_built_capability(
144        spec: &LeanBuiltCapability,
145        imports: impl IntoIterator<Item = impl Into<String>>,
146    ) -> Result<Self, LeanWorkerError> {
147        let artifact = WorkerCapabilityArtifact::from_built_capability(spec)?;
148        let project_root = infer_lake_project_root_from_dylib(&artifact.dylib_path)?;
149        Ok(Self {
150            project_root,
151            import_workspace_root: None,
152            package: artifact.package,
153            lib_name: artifact.module,
154            imports: imports.into_iter().map(Into::into).collect(),
155            import_profile: LeanWorkerSessionImportProfile::default(),
156            built_dylib_path: Some(artifact.dylib_path),
157            built_manifest_path: artifact.manifest_path,
158            built_capability: Some(spec.clone()),
159            worker_child: None,
160            startup_timeout: None,
161            request_timeout: None,
162            shutdown_timeout: None,
163            restart_policy: None,
164            rss_hard_limit: None,
165            module_cache_limits: None,
166            num_threads: None,
167            lean_max_memory_kib: None,
168            metadata_check: None,
169            max_frame_bytes: None,
170            worker_export_signatures: Vec::new(),
171        })
172    }
173
174    /// Use an explicit `lean-rs-worker-child` executable.
175    ///
176    /// Tests and packaged applications should use this when the worker child
177    /// is not discoverable beside the current executable.
178    #[must_use]
179    pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
180        self.worker_child = Some(LeanWorkerChild::path(path));
181        self
182    }
183
184    /// Resolve the worker executable with a packaged worker-child locator.
185    #[must_use]
186    pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
187        self.worker_child = Some(child);
188        self
189    }
190
191    /// Use a separate target Lake workspace root for session imports.
192    ///
193    /// The capability dylib and manifest still come from this builder's
194    /// capability project. This root is the single target workspace whose own
195    /// `.lake/build/lib/lean` entry and `lake-manifest.json` dependency closure
196    /// the worker session imports against. It is not merged with the
197    /// capability project's search path.
198    ///
199    /// Tools whose capability project and audited workspace are distinct must
200    /// set this explicitly. Otherwise the session imports against the
201    /// capability project, preserving the legacy single-project behavior.
202    ///
203    /// Capability exports that import modules must rely on the host-installed
204    /// search path. They must not call `Lean.initSearchPath` or rebuild the
205    /// search path from `LEAN_PATH`, because doing so resets Lean's search path
206    /// and discards this target workspace root.
207    #[must_use]
208    pub fn import_workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
209        self.import_workspace_root = Some(normalize_import_workspace_root(path.into()));
210        self
211    }
212
213    /// Select the full-session import profile used for worker host sessions.
214    #[must_use]
215    pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
216        self.import_profile = profile;
217        self
218    }
219
220    /// Set the maximum time to wait for worker startup.
221    #[must_use]
222    pub fn startup_timeout(mut self, timeout: Duration) -> Self {
223        self.startup_timeout = Some(timeout);
224        self
225    }
226
227    /// Set the maximum time to wait for one worker request.
228    #[must_use]
229    pub fn request_timeout(mut self, timeout: Duration) -> Self {
230        self.request_timeout = Some(timeout);
231        self
232    }
233
234    /// Set the maximum time to wait for graceful worker shutdown.
235    #[must_use]
236    pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
237        self.shutdown_timeout = Some(timeout);
238        self
239    }
240
241    /// Use the documented long-running request timeout profile.
242    #[must_use]
243    pub fn long_running_requests(mut self) -> Self {
244        self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
245        self
246    }
247
248    /// Set the worker restart policy used after startup.
249    #[must_use]
250    pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
251        self.restart_policy = Some(policy);
252        self
253    }
254
255    /// Configure the parent-side hard RSS kill watchdog for in-flight worker
256    /// requests.
257    #[must_use]
258    pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
259        self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
260        self
261    }
262
263    /// Set typed limits for the worker child's module snapshot cache.
264    ///
265    /// These are deliberately not exposed as a generic child-env passthrough:
266    /// the cache knobs are part of the worker lifecycle contract, and callers
267    /// should not need to know the child process's environment-variable names.
268    #[must_use]
269    pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
270        self.module_cache_limits = Some(limits);
271        self
272    }
273
274    /// Cap the worker child's Lean task-manager thread pool via `LEAN_RS_NUM_THREADS`.
275    ///
276    /// Each worker child otherwise starts one task-manager worker thread per hardware core. When
277    /// several children run, or a single elaboration is memory-heavy, that thread pool multiplies
278    /// memory pressure. This is a typed knob (not a generic child-env passthrough): callers set a
279    /// thread count without knowing the child's environment-variable names.
280    #[must_use]
281    pub fn num_threads(mut self, threads: u32) -> Self {
282        self.num_threads = Some(threads);
283        self
284    }
285
286    /// Set an opt-in Lean runtime memory ceiling for the worker child via
287    /// `LEAN_RS_LEAN_MAX_MEMORY_KIB`.
288    ///
289    /// Lean checks this ceiling periodically and throws before the OS OOM-kills the process,
290    /// converting a runaway elaboration into a recoverable error instead of a machine crash.
291    #[must_use]
292    pub fn lean_max_memory_kib(mut self, limit_kib: u64) -> Self {
293        self.lean_max_memory_kib = Some(limit_kib);
294        self
295    }
296
297    /// Set the per-frame byte cap negotiated with the worker child at handshake.
298    ///
299    /// See [`LeanWorkerConfig::max_frame_bytes`] for the policy and the
300    /// `[MIN_FRAME_BYTES, MAX_FRAME_BYTES_HARD_CAP]` clamp. Raise this for
301    /// capabilities whose single logical result composes into one frame
302    /// (e.g. an outline of an entire module, a file-scoped diagnostics
303    /// snapshot) and would otherwise trip `FrameTooLarge`.
304    #[must_use]
305    pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
306        self.max_frame_bytes = Some(max_frame_bytes);
307        self
308    }
309
310    /// Validate generic capability metadata after the session opens.
311    ///
312    /// The export must have ABI `String -> IO String`, matching
313    /// `LeanWorkerSession::capability_metadata`. The returned metadata is
314    /// stored on the opened capability for callers that need it.
315    #[must_use]
316    pub fn validate_metadata(mut self, export: impl Into<String>, request: Value) -> Self {
317        let export = export.into();
318        self.add_worker_export_signature(metadata_signature(export.clone()));
319        self.metadata_check = Some(CapabilityMetadataCheck {
320            export,
321            request,
322            expected: None,
323        });
324        self
325    }
326
327    /// Validate that a capability metadata export returns the expected facts.
328    ///
329    /// This is the pool-facing metadata expectation hook. The metadata remains
330    /// downstream-defined; `lean-rs-worker` only checks that the generic
331    /// metadata envelope matches the caller's requested expectation.
332    #[must_use]
333    pub fn expect_metadata(
334        mut self,
335        export: impl Into<String>,
336        request: Value,
337        expected: LeanWorkerCapabilityMetadata,
338    ) -> Self {
339        let export = export.into();
340        self.add_worker_export_signature(metadata_signature(export.clone()));
341        self.metadata_check = Some(CapabilityMetadataCheck {
342            export,
343            request,
344            expected: Some(expected),
345        });
346        self
347    }
348
349    /// Trust one manifest-backed metadata export with ABI `String -> IO String`.
350    #[must_use]
351    pub fn metadata_export(mut self, export: impl Into<String>) -> Self {
352        self.add_worker_export_signature(metadata_signature(export));
353        self
354    }
355
356    /// Trust one manifest-backed doctor export with ABI `String -> IO String`.
357    #[must_use]
358    pub fn doctor_export(mut self, export: impl Into<String>) -> Self {
359        self.add_worker_export_signature(doctor_signature(export));
360        self
361    }
362
363    /// Trust one manifest-backed JSON command export with ABI `String -> IO String`.
364    #[must_use]
365    pub fn json_command_export(mut self, export: impl Into<String>) -> Self {
366        self.add_worker_export_signature(json_command_signature(export));
367        self
368    }
369
370    /// Trust one manifest-backed streaming command export with ABI `String, USize, USize -> IO UInt8`.
371    #[must_use]
372    pub fn streaming_command_export(mut self, export: impl Into<String>) -> Self {
373        self.add_worker_export_signature(streaming_command_signature(export));
374        self
375    }
376
377    fn add_worker_export_signature(&mut self, signature: LeanExportSignature) {
378        if self
379            .worker_export_signatures
380            .iter()
381            .all(|existing| existing.symbol() != signature.symbol())
382        {
383            self.worker_export_signatures.push(signature);
384        }
385    }
386
387    /// Return the session reuse key represented by this builder.
388    ///
389    /// The key is for worker-pool reuse only. It is not a downstream cache key
390    /// and does not encode row schemas, ranking, reporting, or source
391    /// provenance.
392    #[must_use]
393    pub fn session_key(&self) -> LeanWorkerSessionKey {
394        let restart_policy_class = match &self.restart_policy {
395            Some(policy) if policy == &LeanWorkerRestartPolicy::default() => LeanWorkerRestartPolicyClass::Default,
396            Some(_policy) => LeanWorkerRestartPolicyClass::Custom,
397            None => LeanWorkerRestartPolicyClass::Default,
398        };
399        let mut key = LeanWorkerSessionKey::new(
400            self.project_root.clone(),
401            self.package.clone(),
402            self.lib_name.clone(),
403            self.imports.clone(),
404        )
405        .with_import_profile(self.import_profile)
406        .with_import_workspace_root(self.effective_import_workspace_root())
407        .restart_policy_class(restart_policy_class);
408        if let Some(manifest_path) = &self.built_manifest_path {
409            key = key.with_built_manifest_path(manifest_path.clone());
410        }
411        if let Some(check) = &self.metadata_check {
412            key = key.metadata_expectation(check.export.clone(), check.request.clone(), check.expected.clone());
413        }
414        key
415    }
416
417    fn effective_import_workspace_root(&self) -> PathBuf {
418        self.import_workspace_root
419            .clone()
420            .unwrap_or_else(|| normalize_import_workspace_root(self.project_root.clone()))
421    }
422
423    pub(crate) fn pool_request_timeout(&self) -> Duration {
424        self.request_timeout
425            .unwrap_or(crate::supervisor::LEAN_WORKER_REQUEST_TIMEOUT_DEFAULT)
426    }
427
428    /// Check deployment facts before running a real worker command.
429    ///
430    /// The report validates the worker child locator, manifest-backed
431    /// capability artifact when present, worker protocol handshake, session
432    /// opening, and optional metadata expectation. It keeps child paths,
433    /// protocol frames, and loader environment details below the worker
434    /// boundary.
435    #[must_use]
436    pub fn check(&self) -> LeanWorkerBootstrapReport {
437        let mut checks = self.bootstrap_static_checks();
438        if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
439            return LeanWorkerBootstrapReport::new(checks);
440        }
441
442        match self.clone().open_unchecked() {
443            Ok(capability) => {
444                drop(capability.shutdown());
445            }
446            Err(err) => checks.push(check_from_open_error(&err)),
447        }
448        LeanWorkerBootstrapReport::new(checks)
449    }
450
451    fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
452        let mut checks = Vec::new();
453        checks.extend(worker_child_static_checks(self.worker_child.as_ref()));
454
455        if let Some(spec) = &self.built_capability
456            && let Ok(manifest_path) = spec.resolved_manifest_path()
457        {
458            let report = lean_toolchain::manifest_validation::check_static(&manifest_path);
459            for check in report.errors() {
460                checks.push(LeanWorkerBootstrapCheck::error(
461                    LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight { code: check.code() },
462                    check.subject().to_owned(),
463                    check.message().to_owned(),
464                    check.repair_hint().to_owned(),
465                ));
466            }
467        }
468        checks
469    }
470
471    /// Build the Lake target, start the worker, open the session, and return a ready capability.
472    ///
473    /// # Errors
474    ///
475    /// Returns `LeanWorkerError` if Lake cannot build the target, the worker
476    /// child cannot be resolved or spawned, the worker fails startup/health,
477    /// the session cannot open, or metadata validation fails.
478    pub fn open(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
479        let report = self.bootstrap_static_report();
480        if let Some(check) = report.first_error() {
481            return Err(LeanWorkerError::Bootstrap {
482                code: check.code(),
483                message: check.message().to_owned(),
484            });
485        }
486        self.open_unchecked()
487    }
488
489    fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
490        LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
491    }
492
493    fn open_unchecked(self) -> Result<LeanWorkerCapability, LeanWorkerError> {
494        let import_workspace_root = self.effective_import_workspace_root();
495        let capability_load_started = Instant::now();
496        let (dylib_path, manifest_path) = match (self.built_dylib_path, self.built_manifest_path) {
497            (Some(dylib_path), Some(manifest_path)) => (dylib_path, manifest_path),
498            (_, None) => {
499                let mut builder = lean_toolchain::CargoLeanCapability::new(&self.project_root, &self.lib_name)
500                    .package(&self.package)
501                    .module(&self.lib_name);
502                for signature in self.worker_export_signatures {
503                    builder = builder.export_signature(signature);
504                }
505                let built = builder
506                    .build_quiet()
507                    .map_err(|diagnostic| LeanWorkerError::CapabilityBuild { diagnostic })?;
508                (built.dylib_path().to_path_buf(), built.manifest_path().to_path_buf())
509            }
510            (None, Some(manifest_path)) => {
511                let artifact = WorkerCapabilityArtifact::from_manifest(&manifest_path)?;
512                (artifact.dylib_path, manifest_path)
513            }
514        };
515        let capability_load_elapsed = capability_load_started.elapsed();
516        let mut worker = spawn_checked_worker(
517            self.worker_child,
518            self.startup_timeout,
519            self.request_timeout,
520            self.shutdown_timeout,
521            self.restart_policy,
522            self.rss_hard_limit,
523            self.module_cache_limits,
524            self.max_frame_bytes,
525            self.num_threads,
526            self.lean_max_memory_kib,
527        )?;
528
529        let session_config = LeanWorkerSessionConfig::manifest_backed(
530            import_workspace_root,
531            self.package.clone(),
532            self.lib_name.clone(),
533            manifest_path,
534            self.imports.clone(),
535        )
536        .with_import_profile(self.import_profile);
537
538        let session_open_import_elapsed;
539        let validated_metadata = {
540            let session_open_started = Instant::now();
541            let mut session = worker.open_session(&session_config, None, None)?;
542            session_open_import_elapsed = session_open_started.elapsed();
543            match self.metadata_check {
544                Some(check) => {
545                    let metadata = session.capability_metadata(&check.export, &check.request, None, None)?;
546                    if let Some(expected) = check.expected
547                        && metadata != expected
548                    {
549                        return Err(LeanWorkerError::CapabilityMetadataMismatch {
550                            export: check.export,
551                            expected: Box::new(expected),
552                            actual: Box::new(metadata),
553                        });
554                    }
555                    Some(metadata)
556                }
557                None => None,
558            }
559        };
560        worker.record_capability_open_timing(capability_load_elapsed, session_open_import_elapsed);
561
562        Ok(LeanWorkerCapability {
563            worker,
564            session_config,
565            dylib_path,
566            validated_metadata,
567        })
568    }
569}
570
571/// Builder for a worker-backed host session that loads only bundled shims.
572///
573/// This is the bootstrap path for tools that use the standard host services
574/// exposed through `lean-rs-host`: Meta queries, elaboration, kernel checking,
575/// declaration listing, source ranges, and info trees. It deliberately has no
576/// package/library fields and no metadata validation hook because no user
577/// `@[export]` dylib is built or opened.
578#[derive(Clone, Debug)]
579pub struct LeanWorkerHostHandleBuilder {
580    project_root: PathBuf,
581    imports: Vec<String>,
582    import_profile: LeanWorkerSessionImportProfile,
583    worker_child: Option<LeanWorkerChild>,
584    startup_timeout: Option<Duration>,
585    request_timeout: Option<Duration>,
586    shutdown_timeout: Option<Duration>,
587    restart_policy: Option<LeanWorkerRestartPolicy>,
588    rss_hard_limit: Option<(u64, Duration)>,
589    module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
590    num_threads: Option<u32>,
591    lean_max_memory_kib: Option<u64>,
592    max_frame_bytes: Option<u32>,
593}
594
595/// Typed limits for the worker child's module snapshot cache.
596///
597/// The worker child still receives these values as environment variables at
598/// launch time, but the public API names the lifecycle policy rather than the
599/// transport mechanism. A field left unset uses the child default.
600#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
601pub struct LeanWorkerModuleCacheLimits {
602    max_entries: Option<u64>,
603    ttl_millis: Option<u64>,
604    max_bytes: Option<u64>,
605    rss_guard_kib: Option<u64>,
606    verify_rss_taint_kib: Option<u64>,
607}
608
609impl LeanWorkerModuleCacheLimits {
610    /// Set the maximum retained cache entries.
611    #[must_use]
612    pub fn max_entries(mut self, max_entries: u64) -> Self {
613        self.max_entries = Some(max_entries.max(1));
614        self
615    }
616
617    /// Set the time-to-live for retained module snapshots.
618    #[must_use]
619    pub fn ttl(mut self, ttl: Duration) -> Self {
620        self.ttl_millis = Some(u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX).max(1));
621        self
622    }
623
624    /// Set the approximate retained-cache byte ceiling.
625    #[must_use]
626    pub fn max_bytes(mut self, max_bytes: u64) -> Self {
627        self.max_bytes = Some(max_bytes.max(1));
628        self
629    }
630
631    /// Set the child RSS guard above which the child clears retained snapshots
632    /// before cacheable module-query requests.
633    #[must_use]
634    pub fn rss_guard_kib(mut self, rss_guard_kib: u64) -> Self {
635        self.rss_guard_kib = Some(rss_guard_kib.max(1));
636        self
637    }
638
639    /// Set the child RSS ceiling at or above which a non-positive
640    /// `verify_declaration` verdict (e.g. `NotFound`) is relabeled to
641    /// `BudgetExceeded`: near the cap the worker cannot distinguish a genuine
642    /// "name absent" from an elaboration silently degraded by memory pressure.
643    /// Leave unset (the default) to disable the taint; set it well above the
644    /// warm mathlib baseline so genuine name-absent queries are not mislabeled.
645    #[must_use]
646    pub fn verify_rss_taint_kib(mut self, verify_rss_taint_kib: u64) -> Self {
647        self.verify_rss_taint_kib = Some(verify_rss_taint_kib.max(1));
648        self
649    }
650}
651
652impl LeanWorkerHostHandleBuilder {
653    /// Create a shims-only worker host-session builder for a Lake project.
654    ///
655    /// `project_root` is the directory containing `lakefile.lean`. `imports`
656    /// are the modules to import when the builder performs its initial session
657    /// open. The builder does not build a Lake `:shared` target.
658    #[must_use]
659    pub fn shims_only(project_root: impl Into<PathBuf>, imports: impl IntoIterator<Item = impl Into<String>>) -> Self {
660        Self {
661            project_root: project_root.into(),
662            imports: imports.into_iter().map(Into::into).collect(),
663            import_profile: LeanWorkerSessionImportProfile::default(),
664            worker_child: None,
665            startup_timeout: None,
666            request_timeout: None,
667            shutdown_timeout: None,
668            restart_policy: None,
669            rss_hard_limit: None,
670            module_cache_limits: None,
671            num_threads: None,
672            lean_max_memory_kib: None,
673            max_frame_bytes: None,
674        }
675    }
676
677    /// Use an explicit `lean-rs-worker-child` executable.
678    #[must_use]
679    pub fn worker_executable(mut self, path: impl Into<PathBuf>) -> Self {
680        self.worker_child = Some(LeanWorkerChild::path(path));
681        self
682    }
683
684    /// Select the full-session import profile used for opened host sessions.
685    #[must_use]
686    pub fn import_profile(mut self, profile: LeanWorkerSessionImportProfile) -> Self {
687        self.import_profile = profile;
688        self
689    }
690
691    /// Resolve the worker executable with a packaged worker-child locator.
692    #[must_use]
693    pub fn worker_child(mut self, child: LeanWorkerChild) -> Self {
694        self.worker_child = Some(child);
695        self
696    }
697
698    /// Set the maximum time to wait for worker startup.
699    #[must_use]
700    pub fn startup_timeout(mut self, timeout: Duration) -> Self {
701        self.startup_timeout = Some(timeout);
702        self
703    }
704
705    /// Set the maximum time to wait for one worker request.
706    #[must_use]
707    pub fn request_timeout(mut self, timeout: Duration) -> Self {
708        self.request_timeout = Some(timeout);
709        self
710    }
711
712    /// Set the maximum time to wait for graceful worker shutdown.
713    #[must_use]
714    pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
715        self.shutdown_timeout = Some(timeout);
716        self
717    }
718
719    /// Use the documented long-running request timeout profile.
720    #[must_use]
721    pub fn long_running_requests(mut self) -> Self {
722        self.request_timeout = Some(LEAN_WORKER_REQUEST_TIMEOUT_LONG_RUNNING);
723        self
724    }
725
726    /// Set the worker restart policy used after startup.
727    #[must_use]
728    pub fn restart_policy(mut self, policy: LeanWorkerRestartPolicy) -> Self {
729        self.restart_policy = Some(policy);
730        self
731    }
732
733    /// Configure the parent-side hard RSS kill watchdog for in-flight worker
734    /// requests.
735    #[must_use]
736    pub fn rss_hard_limit(mut self, limit_kib: u64, sample_interval: Duration) -> Self {
737        self.rss_hard_limit = Some((limit_kib.max(1), sample_interval.max(Duration::from_millis(1))));
738        self
739    }
740
741    /// Set typed limits for the worker child's module snapshot cache.
742    ///
743    /// These are applied when the worker child is spawned. They are scoped to
744    /// this handle and do not mutate process-global environment variables.
745    #[must_use]
746    pub fn module_cache_limits(mut self, limits: LeanWorkerModuleCacheLimits) -> Self {
747        self.module_cache_limits = Some(limits);
748        self
749    }
750
751    /// Cap the worker child's Lean task-manager thread pool via `LEAN_RS_NUM_THREADS`.
752    ///
753    /// Scoped to this handle; does not mutate process-global environment variables.
754    #[must_use]
755    pub fn num_threads(mut self, threads: u32) -> Self {
756        self.num_threads = Some(threads);
757        self
758    }
759
760    /// Set an opt-in Lean runtime memory ceiling via `LEAN_RS_LEAN_MAX_MEMORY_KIB`.
761    ///
762    /// Lean throws before the OS OOM-kills the child. Scoped to this handle.
763    #[must_use]
764    pub fn lean_max_memory_kib(mut self, limit_kib: u64) -> Self {
765        self.lean_max_memory_kib = Some(limit_kib);
766        self
767    }
768
769    /// Set the per-frame byte cap negotiated with the worker child at handshake.
770    #[must_use]
771    pub fn max_frame_bytes(mut self, max_frame_bytes: u32) -> Self {
772        self.max_frame_bytes = Some(max_frame_bytes);
773        self
774    }
775
776    /// Check worker bootstrap facts before running a real command.
777    ///
778    /// The report validates the worker child locator, protocol handshake, and
779    /// shims-only session opening. It never builds a user shared-library target.
780    #[must_use]
781    pub fn check(&self) -> LeanWorkerBootstrapReport {
782        let mut checks = self.bootstrap_static_checks();
783        if checks.iter().any(LeanWorkerBootstrapCheck::is_error) {
784            return LeanWorkerBootstrapReport::new(checks);
785        }
786
787        match self.clone().open_unchecked() {
788            Ok(handle) => {
789                drop(handle.shutdown());
790            }
791            Err(err) => checks.push(check_from_open_error(&err)),
792        }
793        LeanWorkerBootstrapReport::new(checks)
794    }
795
796    /// Start the worker, open a shims-only host session once, and return a ready handle.
797    ///
798    /// # Errors
799    ///
800    /// Returns `LeanWorkerError` if the worker child cannot be resolved or
801    /// spawned, startup/health fails, or the shims-only session cannot open.
802    /// This method does not build a user Lake shared-library target.
803    pub fn open(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
804        let report = self.bootstrap_static_report();
805        if let Some(check) = report.first_error() {
806            return Err(LeanWorkerError::Bootstrap {
807                code: check.code(),
808                message: check.message().to_owned(),
809            });
810        }
811        self.open_unchecked()
812    }
813
814    fn bootstrap_static_report(&self) -> LeanWorkerBootstrapReport {
815        LeanWorkerBootstrapReport::new(self.bootstrap_static_checks())
816    }
817
818    fn bootstrap_static_checks(&self) -> Vec<LeanWorkerBootstrapCheck> {
819        worker_child_static_checks(self.worker_child.as_ref())
820    }
821
822    fn open_unchecked(self) -> Result<LeanWorkerHostHandle, LeanWorkerError> {
823        let mut worker = spawn_checked_worker(
824            self.worker_child,
825            self.startup_timeout,
826            self.request_timeout,
827            self.shutdown_timeout,
828            self.restart_policy,
829            self.rss_hard_limit,
830            self.module_cache_limits,
831            self.max_frame_bytes,
832            self.num_threads,
833            self.lean_max_memory_kib,
834        )?;
835        let session_config = LeanWorkerSessionConfig::shims_only(self.project_root, self.imports)
836            .with_import_profile(self.import_profile);
837        {
838            let _session = worker.open_session(&session_config, None, None)?;
839        }
840        Ok(LeanWorkerHostHandle { worker, session_config })
841    }
842}
843
844/// Stable worker bootstrap diagnostic codes.
845#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
846pub enum LeanWorkerBootstrapDiagnosticCode {
847    /// The worker child locator did not resolve to a file.
848    WorkerChildUnresolved,
849    /// The worker child exists but is not executable.
850    WorkerChildNotExecutable,
851    /// Manifest-backed capability preflight reported a loader/artifact issue.
852    CapabilityPreflight { code: LeanLoaderDiagnosticCode },
853    /// The worker child did not complete the protocol handshake.
854    WorkerHandshakeFailed,
855    /// Capability metadata did not match the caller's expectation.
856    CapabilityMetadataMismatch,
857    /// Worker bootstrap failed for a reason outside the named deployment checks.
858    WorkerStartupFailed,
859}
860
861impl LeanWorkerBootstrapDiagnosticCode {
862    /// Stable string identifier suitable for logs and support reports.
863    #[must_use]
864    pub const fn as_str(self) -> &'static str {
865        match self {
866            Self::WorkerChildUnresolved => "lean_rs.worker.bootstrap.child_unresolved",
867            Self::WorkerChildNotExecutable => "lean_rs.worker.bootstrap.child_not_executable",
868            Self::CapabilityPreflight { code } => code.as_str(),
869            Self::WorkerHandshakeFailed => "lean_rs.worker.bootstrap.handshake_failed",
870            Self::CapabilityMetadataMismatch => "lean_rs.worker.bootstrap.metadata_mismatch",
871            Self::WorkerStartupFailed => "lean_rs.worker.bootstrap.startup_failed",
872        }
873    }
874}
875
876impl std::fmt::Display for LeanWorkerBootstrapDiagnosticCode {
877    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878        f.write_str(self.as_str())
879    }
880}
881
882/// Severity of one worker bootstrap finding.
883#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
884pub enum LeanWorkerBootstrapSeverity {
885    /// Informational finding that does not block startup.
886    Info,
887    /// Suspicious state that may still start.
888    Warning,
889    /// The worker should not start real commands until this is fixed.
890    Error,
891}
892
893/// One bounded worker bootstrap finding.
894#[derive(Clone, Debug, Eq, PartialEq)]
895pub struct LeanWorkerBootstrapCheck {
896    code: LeanWorkerBootstrapDiagnosticCode,
897    severity: LeanWorkerBootstrapSeverity,
898    subject: String,
899    message: String,
900    repair_hint: String,
901}
902
903impl LeanWorkerBootstrapCheck {
904    fn error(
905        code: LeanWorkerBootstrapDiagnosticCode,
906        subject: impl Into<String>,
907        message: impl Into<String>,
908        repair_hint: impl Into<String>,
909    ) -> Self {
910        Self {
911            code,
912            severity: LeanWorkerBootstrapSeverity::Error,
913            subject: bound_bootstrap_text(subject.into()),
914            message: bound_bootstrap_text(message.into()),
915            repair_hint: bound_bootstrap_text(repair_hint.into()),
916        }
917    }
918
919    /// Stable diagnostic code.
920    #[must_use]
921    pub fn code(&self) -> LeanWorkerBootstrapDiagnosticCode {
922        self.code
923    }
924
925    /// Whether this finding blocks worker startup.
926    #[must_use]
927    pub fn severity(&self) -> LeanWorkerBootstrapSeverity {
928        self.severity
929    }
930
931    /// Child binary, artifact, export, or protocol step this finding concerns.
932    #[must_use]
933    pub fn subject(&self) -> &str {
934        &self.subject
935    }
936
937    /// Bounded explanation of the finding.
938    #[must_use]
939    pub fn message(&self) -> &str {
940        &self.message
941    }
942
943    /// Bounded repair hint for packaged applications.
944    #[must_use]
945    pub fn repair_hint(&self) -> &str {
946        &self.repair_hint
947    }
948
949    fn is_error(&self) -> bool {
950        self.severity == LeanWorkerBootstrapSeverity::Error
951    }
952}
953
954/// Structured result of worker bootstrap checks for one capability builder.
955#[derive(Clone, Debug, Eq, PartialEq)]
956pub struct LeanWorkerBootstrapReport {
957    checks: Vec<LeanWorkerBootstrapCheck>,
958}
959
960impl LeanWorkerBootstrapReport {
961    fn new(checks: Vec<LeanWorkerBootstrapCheck>) -> Self {
962        Self { checks }
963    }
964
965    /// All bootstrap findings.
966    #[must_use]
967    pub fn checks(&self) -> &[LeanWorkerBootstrapCheck] {
968        &self.checks
969    }
970
971    /// Blocking bootstrap findings.
972    pub fn errors(&self) -> impl Iterator<Item = &LeanWorkerBootstrapCheck> {
973        self.checks
974            .iter()
975            .filter(|check| check.severity == LeanWorkerBootstrapSeverity::Error)
976    }
977
978    /// Whether the worker bootstrap checks found no blocking findings.
979    #[must_use]
980    pub fn is_ok(&self) -> bool {
981        self.first_error().is_none()
982    }
983
984    /// First blocking finding, if any.
985    #[must_use]
986    pub fn first_error(&self) -> Option<&LeanWorkerBootstrapCheck> {
987        self.errors().next()
988    }
989}
990
991/// A worker-backed capability with its Lake target built and worker started.
992///
993/// The value owns the worker supervisor and the session configuration. It is
994/// the normal entry point for downstream capability use until the typed command
995/// facade lands on top of it.
996#[derive(Debug)]
997pub struct LeanWorkerCapability {
998    worker: LeanWorker,
999    session_config: LeanWorkerSessionConfig,
1000    dylib_path: PathBuf,
1001    validated_metadata: Option<LeanWorkerCapabilityMetadata>,
1002}
1003
1004impl LeanWorkerCapability {
1005    /// Open a worker session for this capability.
1006    ///
1007    /// The builder has already proved that the session can open. This method
1008    /// is still fallible because worker cycling, cancellation, or a child
1009    /// failure may require a fresh session.
1010    ///
1011    /// # Errors
1012    ///
1013    /// Returns `LeanWorkerError` if the worker is dead, the child cannot open
1014    /// the configured imports, cancellation is already requested, a progress
1015    /// sink panics, or protocol communication fails.
1016    pub fn open_session(
1017        &mut self,
1018        cancellation: Option<&LeanWorkerCancellationToken>,
1019        progress: Option<&dyn LeanWorkerProgressSink>,
1020    ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1021        self.worker.open_session(&self.session_config, cancellation, progress)
1022    }
1023
1024    pub(crate) fn attach_open_session(&mut self) -> LeanWorkerSession<'_> {
1025        self.worker.attach_open_session()
1026    }
1027
1028    /// Open a worker session with a caller-supplied import set, overriding the imports
1029    /// the builder was constructed with. The capability's `project_root` / `package` /
1030    /// `lib_name` are unchanged.
1031    ///
1032    /// Lifecycle is identical to [`open_session`](Self::open_session): the returned
1033    /// session borrows from `&mut self` and dies when dropped.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Same as [`open_session`](Self::open_session).
1038    pub fn open_session_with_imports(
1039        &mut self,
1040        imports: impl IntoIterator<Item = impl Into<String>>,
1041        cancellation: Option<&LeanWorkerCancellationToken>,
1042        progress: Option<&dyn LeanWorkerProgressSink>,
1043    ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1044        let config = self.session_config.with_imports(imports);
1045        self.worker.open_session(&config, cancellation, progress)
1046    }
1047
1048    /// Return the built capability dylib path resolved by `lean-toolchain`.
1049    #[must_use]
1050    pub fn dylib_path(&self) -> &Path {
1051        &self.dylib_path
1052    }
1053
1054    /// Return the session configuration used by this capability.
1055    #[must_use]
1056    pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1057        &self.session_config
1058    }
1059
1060    /// Return capability metadata validated by the builder, if requested.
1061    #[must_use]
1062    pub fn validated_metadata(&self) -> Option<&LeanWorkerCapabilityMetadata> {
1063        self.validated_metadata.as_ref()
1064    }
1065
1066    /// Return protocol/runtime facts captured from the worker handshake.
1067    #[must_use]
1068    pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1069        self.worker.runtime_metadata()
1070    }
1071
1072    /// Return a snapshot of worker lifecycle counters.
1073    #[must_use]
1074    pub fn stats(&self) -> LeanWorkerStats {
1075        self.worker.stats()
1076    }
1077
1078    /// Return policy-facing lifecycle facts for this worker.
1079    #[must_use]
1080    pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1081        self.worker.lifecycle_snapshot()
1082    }
1083
1084    /// Return the current worker lifecycle status.
1085    ///
1086    /// # Errors
1087    ///
1088    /// Returns `LeanWorkerError` if checking the process status fails.
1089    pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1090        self.worker.status()
1091    }
1092
1093    /// Measure the current child RSS in KiB when supported by the platform.
1094    pub fn rss_kib(&mut self) -> Option<u64> {
1095        self.worker.rss_kib()
1096    }
1097
1098    /// Explicitly cycle the worker process.
1099    ///
1100    /// # Errors
1101    ///
1102    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1103    pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1104        self.worker.cycle()
1105    }
1106
1107    pub(crate) fn cycle_with_restart_reason(&mut self, reason: LeanWorkerRestartReason) -> Result<(), LeanWorkerError> {
1108        self.worker.cycle_with_restart_reason(reason)
1109    }
1110
1111    pub(crate) fn record_command_timing(&mut self, first_command_after_open: bool, elapsed: Duration) {
1112        self.worker.record_command_timing(first_command_after_open, elapsed);
1113    }
1114
1115    /// Set the request timeout for subsequent commands.
1116    pub fn set_request_timeout(&mut self, timeout: Duration) {
1117        self.worker.set_request_timeout(timeout);
1118    }
1119
1120    #[doc(hidden)]
1121    /// Kill the child process for supervisor tests.
1122    ///
1123    /// # Errors
1124    ///
1125    /// Returns `LeanWorkerError` if the worker is already dead or kill fails.
1126    pub fn __kill_for_test(&mut self) -> Result<(), LeanWorkerError> {
1127        self.worker.__kill_for_test()
1128    }
1129
1130    /// Terminate the worker child and return its exit status.
1131    ///
1132    /// # Errors
1133    ///
1134    /// Returns `LeanWorkerError` if the worker is already dead, the terminate
1135    /// request fails, or waiting for the child fails.
1136    #[deprecated(note = "use LeanWorkerCapability::shutdown for structured shutdown status")]
1137    pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1138        self.worker.shutdown().map(|report| report.exit)
1139    }
1140
1141    /// Shut down the worker child and return structured shutdown status.
1142    ///
1143    /// # Errors
1144    ///
1145    /// Returns `LeanWorkerError` if shutdown escalation or wait/reap fails.
1146    pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1147        self.worker.shutdown()
1148    }
1149}
1150
1151/// A worker-backed host session handle that is backed only by bundled shims.
1152///
1153/// Unlike [`LeanWorkerCapability`], this type has no user dylib path and no
1154/// metadata exports. It owns the worker supervisor and a shims-only session
1155/// configuration; each opened session can import project `.olean` files and
1156/// call the standard worker services.
1157#[derive(Debug)]
1158pub struct LeanWorkerHostHandle {
1159    worker: LeanWorker,
1160    session_config: LeanWorkerSessionConfig,
1161}
1162
1163impl LeanWorkerHostHandle {
1164    /// Open a worker session for this host handle.
1165    ///
1166    /// The builder has already proved that the session can open. This method
1167    /// is still fallible because worker cycling, cancellation, or a child
1168    /// failure may require a fresh session.
1169    ///
1170    /// # Errors
1171    ///
1172    /// Returns `LeanWorkerError` if the worker is dead, the child cannot open
1173    /// the configured imports, cancellation is already requested, a progress
1174    /// sink panics, or protocol communication fails.
1175    pub fn open_session(
1176        &mut self,
1177        cancellation: Option<&LeanWorkerCancellationToken>,
1178        progress: Option<&dyn LeanWorkerProgressSink>,
1179    ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1180        self.worker.open_session(&self.session_config, cancellation, progress)
1181    }
1182
1183    /// Open a worker session with a caller-supplied import set, overriding the
1184    /// imports the builder was constructed with.
1185    ///
1186    /// Lifecycle is identical to [`open_session`](Self::open_session): the
1187    /// returned session borrows from `&mut self` and dies when dropped.
1188    ///
1189    /// # Errors
1190    ///
1191    /// Same as [`open_session`](Self::open_session).
1192    pub fn open_session_with_imports(
1193        &mut self,
1194        imports: impl IntoIterator<Item = impl Into<String>>,
1195        cancellation: Option<&LeanWorkerCancellationToken>,
1196        progress: Option<&dyn LeanWorkerProgressSink>,
1197    ) -> Result<LeanWorkerSession<'_>, LeanWorkerError> {
1198        let config = self.session_config.with_imports(imports);
1199        self.worker.open_session(&config, cancellation, progress)
1200    }
1201
1202    fn with_session_imports<T>(
1203        &mut self,
1204        imports: Vec<String>,
1205        cancellation: Option<&LeanWorkerCancellationToken>,
1206        progress: Option<&dyn LeanWorkerProgressSink>,
1207        command: impl Fn(&mut LeanWorkerSession<'_>) -> Result<T, LeanWorkerError>,
1208    ) -> Result<T, LeanWorkerError> {
1209        let result = {
1210            let mut session = self.open_session_with_imports(imports.clone(), cancellation, progress)?;
1211            command(&mut session)
1212        };
1213        match result {
1214            Ok(value) => Ok(value),
1215            Err(err) if worker_session_missing(&err) => {
1216                let mut session = self.open_session_with_imports(imports, cancellation, progress)?;
1217                command(&mut session)
1218            }
1219            Err(err) => Err(err),
1220        }
1221    }
1222
1223    /// Open a session with `imports`, process one module query, and retry once
1224    /// if an automatic worker lifecycle cycle invalidated the just-opened
1225    /// session before the command frame was sent.
1226    ///
1227    /// # Errors
1228    ///
1229    /// Returns `LeanWorkerError` for worker, protocol, cancellation, or
1230    /// progress-sink failures other than the internally retried
1231    /// `session_missing` race.
1232    pub fn process_module_query_with_imports(
1233        &mut self,
1234        imports: Vec<String>,
1235        source: &str,
1236        query: &LeanWorkerModuleQuery,
1237        options: &LeanWorkerElabOptions,
1238        cancellation: Option<&LeanWorkerCancellationToken>,
1239        progress: Option<&dyn LeanWorkerProgressSink>,
1240    ) -> Result<LeanWorkerModuleQueryOutcome, LeanWorkerError> {
1241        self.with_session_imports(imports, cancellation, progress, |session| {
1242            session.process_module_query(source, query.clone(), options, cancellation, progress)
1243        })
1244    }
1245
1246    /// Open a session with `imports`, process one module-query batch, and
1247    /// retry once on the `session_missing` lifecycle race.
1248    ///
1249    /// # Errors
1250    ///
1251    /// Same as [`Self::process_module_query_with_imports`].
1252    pub fn process_module_query_batch_with_imports(
1253        &mut self,
1254        imports: Vec<String>,
1255        source: &str,
1256        selectors: &[LeanWorkerModuleQuerySelector],
1257        budgets: &LeanWorkerOutputBudgets,
1258        options: &LeanWorkerElabOptions,
1259        cancellation: Option<&LeanWorkerCancellationToken>,
1260        progress: Option<&dyn LeanWorkerProgressSink>,
1261    ) -> Result<LeanWorkerModuleQueryBatchOutcome, LeanWorkerError> {
1262        self.with_session_imports(imports, cancellation, progress, |session| {
1263            session.process_module_query_batch(source, selectors, budgets, options, cancellation, progress)
1264        })
1265    }
1266
1267    /// Open a session with `imports` and inspect one declaration.
1268    ///
1269    /// # Errors
1270    ///
1271    /// Same as [`Self::process_module_query_with_imports`].
1272    pub fn inspect_declaration_with_imports(
1273        &mut self,
1274        imports: Vec<String>,
1275        request: &LeanWorkerDeclarationInspectionRequest,
1276        cancellation: Option<&LeanWorkerCancellationToken>,
1277        progress: Option<&dyn LeanWorkerProgressSink>,
1278    ) -> Result<LeanWorkerDeclarationInspectionResult, LeanWorkerError> {
1279        self.with_session_imports(imports, cancellation, progress, |session| {
1280            session.inspect_declaration(request, cancellation, progress)
1281        })
1282    }
1283
1284    /// Open a session with `imports` and run bounded declaration search.
1285    ///
1286    /// # Errors
1287    ///
1288    /// Same as [`Self::process_module_query_with_imports`].
1289    pub fn search_declarations_with_imports(
1290        &mut self,
1291        imports: Vec<String>,
1292        search: &LeanWorkerDeclarationSearch,
1293        cancellation: Option<&LeanWorkerCancellationToken>,
1294        progress: Option<&dyn LeanWorkerProgressSink>,
1295    ) -> Result<LeanWorkerDeclarationSearchResult, LeanWorkerError> {
1296        self.with_session_imports(imports, cancellation, progress, |session| {
1297            session.search_declarations(search, cancellation, progress)
1298        })
1299    }
1300
1301    /// Open a session with `imports` and try proof fragments in-memory.
1302    ///
1303    /// # Errors
1304    ///
1305    /// Same as [`Self::process_module_query_with_imports`].
1306    pub fn attempt_proof_with_imports(
1307        &mut self,
1308        imports: Vec<String>,
1309        request: &LeanWorkerProofAttemptRequest,
1310        options: &LeanWorkerElabOptions,
1311        cancellation: Option<&LeanWorkerCancellationToken>,
1312        progress: Option<&dyn LeanWorkerProgressSink>,
1313    ) -> Result<LeanWorkerProofAttemptResult, LeanWorkerError> {
1314        self.with_session_imports(imports, cancellation, progress, |session| {
1315            session.attempt_proof(request, options, cancellation, progress)
1316        })
1317    }
1318
1319    /// Open a session with `imports` and verify one declaration in-memory.
1320    ///
1321    /// # Errors
1322    ///
1323    /// Same as [`Self::process_module_query_with_imports`].
1324    pub fn verify_declaration_with_imports(
1325        &mut self,
1326        imports: Vec<String>,
1327        request: &LeanWorkerDeclarationVerificationRequest,
1328        options: &LeanWorkerElabOptions,
1329        cancellation: Option<&LeanWorkerCancellationToken>,
1330        progress: Option<&dyn LeanWorkerProgressSink>,
1331    ) -> Result<LeanWorkerDeclarationVerificationResult, LeanWorkerError> {
1332        self.with_session_imports(imports, cancellation, progress, |session| {
1333            session.verify_declaration(request, options, cancellation, progress)
1334        })
1335    }
1336
1337    /// Open a session with `imports` and verify several declarations in one
1338    /// in-memory source snapshot.
1339    ///
1340    /// # Errors
1341    ///
1342    /// Same as [`Self::process_module_query_with_imports`].
1343    pub fn verify_declaration_batch_with_imports(
1344        &mut self,
1345        imports: Vec<String>,
1346        request: &LeanWorkerDeclarationVerificationBatchRequest,
1347        options: &LeanWorkerElabOptions,
1348        cancellation: Option<&LeanWorkerCancellationToken>,
1349        progress: Option<&dyn LeanWorkerProgressSink>,
1350    ) -> Result<LeanWorkerDeclarationVerificationBatchResult, LeanWorkerError> {
1351        self.with_session_imports(imports, cancellation, progress, |session| {
1352            session.verify_declaration_batch(request, options, cancellation, progress)
1353        })
1354    }
1355
1356    /// Return the session configuration used by this host handle.
1357    #[must_use]
1358    pub fn session_config(&self) -> &LeanWorkerSessionConfig {
1359        &self.session_config
1360    }
1361
1362    /// Return protocol/runtime facts captured from the worker handshake.
1363    #[must_use]
1364    pub fn runtime_metadata(&self) -> LeanWorkerRuntimeMetadata {
1365        self.worker.runtime_metadata()
1366    }
1367
1368    /// Return a snapshot of worker lifecycle counters.
1369    #[must_use]
1370    pub fn stats(&self) -> LeanWorkerStats {
1371        self.worker.stats()
1372    }
1373
1374    /// Return policy-facing lifecycle facts for this worker.
1375    #[must_use]
1376    pub fn lifecycle_snapshot(&self) -> LeanWorkerLifecycleSnapshot {
1377        self.worker.lifecycle_snapshot()
1378    }
1379
1380    /// Return the current worker lifecycle status.
1381    ///
1382    /// # Errors
1383    ///
1384    /// Returns `LeanWorkerError` if checking the process status fails.
1385    pub fn status(&mut self) -> Result<LeanWorkerStatus, LeanWorkerError> {
1386        self.worker.status()
1387    }
1388
1389    /// Measure the current child RSS in KiB when supported by the platform.
1390    pub fn rss_kib(&mut self) -> Option<u64> {
1391        self.worker.rss_kib()
1392    }
1393
1394    /// Explicitly cycle the worker process.
1395    ///
1396    /// # Errors
1397    ///
1398    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1399    pub fn cycle(&mut self) -> Result<(), LeanWorkerError> {
1400        self.worker.cycle()
1401    }
1402
1403    /// Restart this worker using its original configuration.
1404    ///
1405    /// # Errors
1406    ///
1407    /// Returns `LeanWorkerError` if the worker cannot be replaced.
1408    pub fn restart(&mut self) -> Result<(), LeanWorkerError> {
1409        self.worker.restart()
1410    }
1411
1412    /// Terminate the worker child and return its exit status.
1413    ///
1414    /// # Errors
1415    ///
1416    /// Returns `LeanWorkerError` if the worker is already dead, the terminate
1417    /// request fails, or waiting for the child fails.
1418    #[deprecated(note = "use LeanWorkerHostHandle::shutdown for structured shutdown status")]
1419    pub fn terminate(self) -> Result<crate::supervisor::LeanWorkerExit, LeanWorkerError> {
1420        self.worker.shutdown().map(|report| report.exit)
1421    }
1422
1423    /// Shut down the worker child and return structured shutdown status.
1424    ///
1425    /// # Errors
1426    ///
1427    /// Returns `LeanWorkerError` if shutdown escalation or wait/reap fails.
1428    pub fn shutdown(self) -> Result<LeanWorkerShutdownReport, LeanWorkerError> {
1429        self.worker.shutdown()
1430    }
1431}
1432
1433fn worker_session_missing(err: &LeanWorkerError) -> bool {
1434    matches!(err, LeanWorkerError::Worker { code, .. } if code == "lean_rs.worker.session_missing")
1435}
1436
1437#[derive(Clone, Debug)]
1438struct CapabilityMetadataCheck {
1439    export: String,
1440    request: Value,
1441    expected: Option<LeanWorkerCapabilityMetadata>,
1442}
1443
1444#[derive(Debug)]
1445struct WorkerCapabilityArtifact {
1446    dylib_path: PathBuf,
1447    manifest_path: Option<PathBuf>,
1448    package: String,
1449    module: String,
1450}
1451
1452impl WorkerCapabilityArtifact {
1453    fn from_built_capability(spec: &LeanBuiltCapability) -> Result<Self, LeanWorkerError> {
1454        if let Ok(manifest_path) = spec.resolved_manifest_path() {
1455            let mut artifact = Self::from_manifest(&manifest_path)?;
1456            artifact.manifest_path = Some(manifest_path);
1457            return Ok(artifact);
1458        }
1459
1460        let dylib_path = spec.dylib_path().map_err(|err| LeanWorkerError::Setup {
1461            message: err.to_string(),
1462        })?;
1463        let package = spec.package_name().ok_or_else(|| LeanWorkerError::Setup {
1464            message: "LeanBuiltCapability is missing the Lake package name; call `.package(...)`".to_owned(),
1465        })?;
1466        let module = spec.module_name().ok_or_else(|| LeanWorkerError::Setup {
1467            message: "LeanBuiltCapability is missing the root Lean module name; call `.module(...)`".to_owned(),
1468        })?;
1469        Ok(Self {
1470            dylib_path,
1471            manifest_path: None,
1472            package: package.to_owned(),
1473            module: module.to_owned(),
1474        })
1475    }
1476
1477    fn from_manifest(manifest_path: &Path) -> Result<Self, LeanWorkerError> {
1478        let bytes = std::fs::read(manifest_path).map_err(|err| LeanWorkerError::Bootstrap {
1479            code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1480                code: LeanLoaderDiagnosticCode::MissingManifest,
1481            },
1482            message: format!(
1483                "could not read Lean capability manifest '{}': {err}",
1484                manifest_path.display()
1485            ),
1486        })?;
1487        let manifest: WorkerCapabilityManifest =
1488            serde_json::from_slice(&bytes).map_err(|err| LeanWorkerError::Bootstrap {
1489                code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1490                    code: LeanLoaderDiagnosticCode::MalformedManifest,
1491                },
1492                message: format!(
1493                    "Lean capability manifest '{}' is malformed: {err}",
1494                    manifest_path.display()
1495                ),
1496            })?;
1497        if manifest.schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
1498            return Err(LeanWorkerError::Bootstrap {
1499                code: LeanWorkerBootstrapDiagnosticCode::CapabilityPreflight {
1500                    code: LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
1501                },
1502                message: format!(
1503                    "unsupported Lean capability manifest schema {}; supported schema is {}",
1504                    manifest.schema_version,
1505                    lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
1506                ),
1507            });
1508        }
1509        Ok(Self {
1510            dylib_path: manifest.primary_dylib,
1511            manifest_path: Some(manifest_path.to_path_buf()),
1512            package: manifest.package,
1513            module: manifest.module,
1514        })
1515    }
1516}
1517
1518#[derive(Deserialize)]
1519struct WorkerCapabilityManifest {
1520    schema_version: u64,
1521    primary_dylib: PathBuf,
1522    package: String,
1523    module: String,
1524}
1525
1526fn worker_child_static_checks(worker_child: Option<&LeanWorkerChild>) -> Vec<LeanWorkerBootstrapCheck> {
1527    let mut checks = Vec::new();
1528    match worker_child.map_or_else(resolve_default_worker_executable, LeanWorkerChild::resolve) {
1529        Ok(path) => {
1530            if let Err(err) = validate_worker_child_path(&path) {
1531                checks.push(check_from_open_error(&err));
1532            }
1533        }
1534        Err(err) => checks.push(check_from_open_error(&err)),
1535    }
1536    checks
1537}
1538
1539fn spawn_checked_worker(
1540    worker_child: Option<LeanWorkerChild>,
1541    startup_timeout: Option<Duration>,
1542    request_timeout: Option<Duration>,
1543    shutdown_timeout: Option<Duration>,
1544    restart_policy: Option<LeanWorkerRestartPolicy>,
1545    rss_hard_limit: Option<(u64, Duration)>,
1546    module_cache_limits: Option<LeanWorkerModuleCacheLimits>,
1547    max_frame_bytes: Option<u32>,
1548    num_threads: Option<u32>,
1549    lean_max_memory_kib: Option<u64>,
1550) -> Result<LeanWorker, LeanWorkerError> {
1551    let worker_child = worker_child.unwrap_or_default();
1552    let worker_executable = worker_child.resolve()?;
1553    validate_worker_child_path(&worker_executable)?;
1554    let lean_sysroot = worker_child.resolve_lean_sysroot()?;
1555
1556    let mut config = LeanWorkerConfig::new(worker_executable).env("LEAN_SYSROOT", lean_sysroot.as_os_str());
1557    if let Some(timeout) = startup_timeout {
1558        config = config.startup_timeout(timeout);
1559    }
1560    if let Some(timeout) = request_timeout {
1561        config = config.request_timeout(timeout);
1562    }
1563    if let Some(timeout) = shutdown_timeout {
1564        config = config.shutdown_timeout(timeout);
1565    }
1566    if let Some(policy) = restart_policy {
1567        config = config.restart_policy(policy);
1568    }
1569    if let Some((limit_kib, sample_interval)) = rss_hard_limit {
1570        config = config.rss_hard_limit(limit_kib, sample_interval);
1571    }
1572    if let Some(limits) = module_cache_limits.as_ref() {
1573        config = apply_module_cache_limits(config, limits);
1574    }
1575    config = apply_runtime_limits(config, num_threads, lean_max_memory_kib);
1576    if let Some(cap) = max_frame_bytes {
1577        config = config.max_frame_bytes(cap);
1578    }
1579
1580    let mut worker = LeanWorker::spawn(&config)?;
1581    worker.health()?;
1582    Ok(worker)
1583}
1584
1585/// Apply the typed worker-child runtime caps as child environment variables.
1586///
1587/// `num_threads` caps the child's Lean task-manager pool (`LEAN_RS_NUM_THREADS`); the memory ceiling
1588/// arms the child's Lean runtime OOM guardrail (`LEAN_RS_LEAN_MAX_MEMORY_KIB`). Both are typed knobs
1589/// so callers need not know the child's environment-variable names.
1590fn apply_runtime_limits(
1591    mut config: LeanWorkerConfig,
1592    num_threads: Option<u32>,
1593    lean_max_memory_kib: Option<u64>,
1594) -> LeanWorkerConfig {
1595    if let Some(threads) = num_threads {
1596        config = config.env("LEAN_RS_NUM_THREADS", threads.to_string());
1597    }
1598    if let Some(limit_kib) = lean_max_memory_kib {
1599        config = config.env("LEAN_RS_LEAN_MAX_MEMORY_KIB", limit_kib.to_string());
1600    }
1601    config
1602}
1603
1604fn apply_module_cache_limits(mut config: LeanWorkerConfig, limits: &LeanWorkerModuleCacheLimits) -> LeanWorkerConfig {
1605    if let Some(value) = limits.max_entries {
1606        config = config.env("LEAN_RS_MODULE_CACHE_MAX_ENTRIES", value.to_string());
1607    }
1608    if let Some(value) = limits.ttl_millis {
1609        config = config.env("LEAN_RS_MODULE_CACHE_TTL_MILLIS", value.to_string());
1610    }
1611    if let Some(value) = limits.max_bytes {
1612        config = config.env("LEAN_RS_MODULE_CACHE_MAX_BYTES", value.to_string());
1613    }
1614    if let Some(value) = limits.rss_guard_kib {
1615        config = config.env("LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB", value.to_string());
1616    }
1617    if let Some(value) = limits.verify_rss_taint_kib {
1618        config = config.env("LEAN_RS_VERIFY_RSS_TAINT_KIB", value.to_string());
1619    }
1620    config
1621}
1622
1623/// Locator for an app-owned worker child executable.
1624///
1625/// Dependency binaries are not automatically installed with downstream
1626/// applications. Production apps should ship a tiny binary that calls
1627/// `lean_rs_worker_child::run_worker_child_stdio` and point the capability
1628/// builder at it through this locator.
1629///
1630/// # Toolchain binding
1631///
1632/// A worker child binary is *built against one Lean toolchain*: its rpath
1633/// points at one `libleanshared`, and `LEAN_SYSROOT` at spawn time must point
1634/// at the matching stdlib oleans (`<sysroot>/lib/lean/Init.olean`). Mismatched
1635/// rpath and sysroot abort with `incompatible header` before the handshake.
1636///
1637/// The locator carries both: the binary path (via [`Self::path`] or
1638/// [`Self::sibling`]) and, optionally, the matching sysroot (via
1639/// [`Self::for_toolchain`] or [`Self::lean_sysroot`]). When the supervisor
1640/// spawns the child, it sets `LEAN_SYSROOT` from the locator (or from
1641/// [`lean_toolchain::discover_toolchain`] as a fallback) so callers never have
1642/// to thread the env var manually.
1643///
1644/// # Design note: no generic `env(key, value)` passthrough
1645///
1646/// `LeanWorkerCapabilityBuilder` and `LeanWorkerChild` deliberately do **not**
1647/// expose a general `env(key, value)` builder. Every environment variable the
1648/// worker child cares about has a typed method whose name describes the
1649/// invariant it enforces (e.g. [`Self::lean_sysroot`] enforces the
1650/// rpath/sysroot match). If a future env var needs to be plumbed through, add
1651/// a typed builder for it—do **not** add a generic `env(...)`. Generic
1652/// passthroughs leak implementation knowledge (env var names, framing
1653/// invariants) into every caller and erode the structural guarantee that
1654/// supported configurations cannot be misconstructed.
1655///
1656/// # Provisioning is the caller's job
1657///
1658/// This locator only *finds* a worker child; it does not install one.
1659/// Dependency binaries are not shipped with downstream applications, so a
1660/// downstream must provision its own worker child — ship it beside the host
1661/// binary (resolved by [`Self::sibling`]), point at it explicitly
1662/// ([`Self::path`] / [`Self::for_toolchain`]), or supply it through an
1663/// [`Self::env_override`] variable. The one exception is a developer
1664/// convenience internal to the `lean-rs` source tree: when the requested name
1665/// is the default `lean-rs-worker-child` *and* resolution runs from within the
1666/// `lean-rs` workspace, [`Self::sibling`] also falls back to building that
1667/// binary with `cargo build -p lean-rs-worker-child` (so the workspace's own
1668/// examples and profiling harness run with no extra steps). That fallback is
1669/// keyed to `lean-rs`'s own `CARGO_MANIFEST_DIR` and is **inert for every
1670/// downstream** — outside the source tree it finds no workspace to build and
1671/// no-ops. Do not rely on it from a consuming crate.
1672#[derive(Clone, Debug, Eq, PartialEq)]
1673pub struct LeanWorkerChild {
1674    executable_name: Option<String>,
1675    explicit_path: Option<PathBuf>,
1676    env_var: Option<String>,
1677    lean_sysroot: Option<PathBuf>,
1678}
1679
1680impl LeanWorkerChild {
1681    /// Locate a worker child beside the current executable, or beside the
1682    /// Cargo profile directory during tests and `cargo run`.
1683    ///
1684    /// For the default name `lean-rs-worker-child`, resolution additionally
1685    /// falls back to building the binary from source — but only inside the
1686    /// `lean-rs` workspace itself. See the [type-level provisioning
1687    /// note](Self#provisioning-is-the-callers-job): that fallback is an
1688    /// in-tree developer convenience and is inert for downstream callers, which
1689    /// must ship or point at their own worker child.
1690    #[must_use]
1691    pub fn sibling(executable_name: impl Into<String>) -> Self {
1692        Self {
1693            executable_name: Some(with_exe_suffix(executable_name.into())),
1694            explicit_path: None,
1695            env_var: None,
1696            lean_sysroot: None,
1697        }
1698    }
1699
1700    /// Use an explicit worker child path.
1701    #[must_use]
1702    pub fn path(path: impl Into<PathBuf>) -> Self {
1703        Self {
1704            executable_name: None,
1705            explicit_path: Some(path.into()),
1706            env_var: None,
1707            lean_sysroot: None,
1708        }
1709    }
1710
1711    /// Locate a worker child and declare the Lean toolchain its rpath was
1712    /// built against.
1713    ///
1714    /// `sysroot` is the Lean prefix containing `lib/lean/Init.olean`. The
1715    /// supervisor sets `LEAN_SYSROOT` to this value when spawning the child,
1716    /// so a single parent process can host multiple workers each pinned to a
1717    /// different toolchain.
1718    #[must_use]
1719    pub fn for_toolchain(path: impl Into<PathBuf>, sysroot: impl Into<PathBuf>) -> Self {
1720        Self {
1721            executable_name: None,
1722            explicit_path: Some(path.into()),
1723            env_var: None,
1724            lean_sysroot: Some(sysroot.into()),
1725        }
1726    }
1727
1728    /// Set or override the Lean sysroot the spawned child uses.
1729    ///
1730    /// When unset, the supervisor falls back to
1731    /// [`lean_toolchain::discover_toolchain`] at spawn time.
1732    #[must_use]
1733    pub fn lean_sysroot(mut self, sysroot: impl Into<PathBuf>) -> Self {
1734        self.lean_sysroot = Some(sysroot.into());
1735        self
1736    }
1737
1738    /// Add an environment-variable override for launchers and tests.
1739    #[must_use]
1740    pub fn env_override(mut self, env_var: impl Into<String>) -> Self {
1741        self.env_var = Some(env_var.into());
1742        self
1743    }
1744
1745    /// Return the sysroot the supervisor will set as `LEAN_SYSROOT`.
1746    ///
1747    /// Returns the explicit sysroot if one was bound via
1748    /// [`Self::for_toolchain`] or [`Self::lean_sysroot`]; otherwise runs
1749    /// [`lean_toolchain::discover_toolchain`] with default options and returns
1750    /// the discovered prefix.
1751    fn resolve_lean_sysroot(&self) -> Result<PathBuf, LeanWorkerError> {
1752        if let Some(sysroot) = &self.lean_sysroot {
1753            return Ok(sysroot.clone());
1754        }
1755        let info = lean_toolchain::discover_toolchain(&lean_toolchain::DiscoverOptions::default()).map_err(|diag| {
1756            LeanWorkerError::Setup {
1757                message: format!("could not discover Lean sysroot for worker spawn: {diag}"),
1758            }
1759        })?;
1760        Ok(info.prefix)
1761    }
1762
1763    fn resolve(&self) -> Result<PathBuf, LeanWorkerError> {
1764        let mut tried = Vec::new();
1765        if let Some(env_var) = &self.env_var
1766            && let Some(value) = env::var_os(env_var)
1767        {
1768            let path = PathBuf::from(value);
1769            if path.is_file() {
1770                return Ok(path);
1771            }
1772            tried.push(path);
1773            return Err(LeanWorkerError::WorkerChildUnresolved { tried });
1774        }
1775        if let Some(path) = &self.explicit_path {
1776            return Ok(path.clone());
1777        }
1778
1779        let executable_name = self
1780            .executable_name
1781            .clone()
1782            .unwrap_or_else(|| with_exe_suffix("lean-rs-worker-child".to_owned()));
1783        tried.extend(candidate_sibling_worker_paths(&executable_name));
1784        // In-tree convenience only: `try_build_workspace_worker_child` can build
1785        // exactly the `lean-rs-worker-child` binary and only when run from the
1786        // `lean-rs` workspace, so it is gated on that name (a differently-named
1787        // sibling would otherwise build the wrong binary) and no-ops downstream.
1788        if executable_name == with_exe_suffix("lean-rs-worker-child".to_owned())
1789            && let Some(path) = try_build_workspace_worker_child(&executable_name, &mut tried)
1790        {
1791            return Ok(path);
1792        }
1793        for path in dedup_paths(&tried) {
1794            if path.is_file() {
1795                return Ok(path);
1796            }
1797        }
1798        Err(LeanWorkerError::WorkerChildUnresolved { tried })
1799    }
1800}
1801
1802impl Default for LeanWorkerChild {
1803    fn default() -> Self {
1804        Self::sibling("lean-rs-worker-child").env_override(WORKER_CHILD_ENV)
1805    }
1806}
1807
1808fn resolve_default_worker_executable() -> Result<PathBuf, LeanWorkerError> {
1809    LeanWorkerChild::default().resolve()
1810}
1811
1812fn validate_worker_child_path(path: &Path) -> Result<(), LeanWorkerError> {
1813    if !path.is_file() {
1814        return Err(LeanWorkerError::WorkerChildNotExecutable {
1815            path: path.to_path_buf(),
1816            reason: "path does not point to a file".to_owned(),
1817        });
1818    }
1819    if !is_executable_file(path) {
1820        return Err(LeanWorkerError::WorkerChildNotExecutable {
1821            path: path.to_path_buf(),
1822            reason: "file is not executable by this user".to_owned(),
1823        });
1824    }
1825    Ok(())
1826}
1827
1828#[cfg(unix)]
1829fn is_executable_file(path: &Path) -> bool {
1830    use std::os::unix::fs::PermissionsExt as _;
1831
1832    std::fs::metadata(path).is_ok_and(|metadata| metadata.permissions().mode() & 0o111 != 0)
1833}
1834
1835#[cfg(not(unix))]
1836fn is_executable_file(_path: &Path) -> bool {
1837    true
1838}
1839
1840fn check_from_open_error(err: &LeanWorkerError) -> LeanWorkerBootstrapCheck {
1841    match err {
1842        LeanWorkerError::WorkerChildUnresolved { tried } => LeanWorkerBootstrapCheck::error(
1843            LeanWorkerBootstrapDiagnosticCode::WorkerChildUnresolved,
1844            "worker child",
1845            format!("could not resolve worker child; tried {}", format_paths(tried)),
1846            "ship an app-owned worker child binary beside the app or configure LeanWorkerChild::env_override",
1847        ),
1848        LeanWorkerError::WorkerChildNotExecutable { path, reason } => LeanWorkerBootstrapCheck::error(
1849            LeanWorkerBootstrapDiagnosticCode::WorkerChildNotExecutable,
1850            path.display().to_string(),
1851            reason.clone(),
1852            "ship an app-owned worker child binary and ensure it is executable",
1853        ),
1854        LeanWorkerError::Bootstrap { code, message } => LeanWorkerBootstrapCheck::error(
1855            *code,
1856            code.as_str(),
1857            message.clone(),
1858            "fix the reported bootstrap input",
1859        ),
1860        LeanWorkerError::Handshake { message } => LeanWorkerBootstrapCheck::error(
1861            LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1862            "worker handshake",
1863            message.clone(),
1864            "ensure the worker child calls lean_rs_worker_child::run_worker_child_stdio and matches this crate version",
1865        ),
1866        LeanWorkerError::Timeout {
1867            operation: "startup", ..
1868        } => LeanWorkerBootstrapCheck::error(
1869            LeanWorkerBootstrapDiagnosticCode::WorkerHandshakeFailed,
1870            "worker handshake",
1871            err.to_string(),
1872            "check that the worker child starts promptly and writes the lean-rs-worker handshake",
1873        ),
1874        LeanWorkerError::CapabilityMetadataMismatch { export, .. } => LeanWorkerBootstrapCheck::error(
1875            LeanWorkerBootstrapDiagnosticCode::CapabilityMetadataMismatch,
1876            export.clone(),
1877            "capability metadata did not match the requested expectation",
1878            "rebuild or select a capability whose metadata matches the caller expectation",
1879        ),
1880        other @ (LeanWorkerError::Spawn { .. }
1881        | LeanWorkerError::CapabilityBuild { .. }
1882        | LeanWorkerError::Setup { .. }
1883        | LeanWorkerError::Protocol { .. }
1884        | LeanWorkerError::Worker { .. }
1885        | LeanWorkerError::ChildExited { .. }
1886        | LeanWorkerError::ChildPanicOrAbort { .. }
1887        | LeanWorkerError::Timeout { .. }
1888        | LeanWorkerError::RssHardLimitExceeded { .. }
1889        | LeanWorkerError::Cancelled { .. }
1890        | LeanWorkerError::ProgressPanic { .. }
1891        | LeanWorkerError::DataSinkPanic { .. }
1892        | LeanWorkerError::DiagnosticSinkPanic { .. }
1893        | LeanWorkerError::StreamExportFailed { .. }
1894        | LeanWorkerError::StreamCallbackFailed { .. }
1895        | LeanWorkerError::StreamRowMalformed { .. }
1896        | LeanWorkerError::CapabilityMetadataMalformed { .. }
1897        | LeanWorkerError::CapabilityDoctorMalformed { .. }
1898        | LeanWorkerError::TypedCommandRequestEncode { .. }
1899        | LeanWorkerError::TypedCommandResponseDecode { .. }
1900        | LeanWorkerError::TypedCommandRowDecode { .. }
1901        | LeanWorkerError::TypedCommandSummaryDecode { .. }
1902        | LeanWorkerError::LeaseInvalidated { .. }
1903        | LeanWorkerError::WorkerPoolExhausted { .. }
1904        | LeanWorkerError::WorkerPoolMemoryBudgetExceeded { .. }
1905        | LeanWorkerError::WorkerPoolQueueTimeout { .. }
1906        | LeanWorkerError::RestartLimitExceeded { .. }
1907        | LeanWorkerError::UnsupportedRequest { .. }
1908        | LeanWorkerError::Wait { .. }
1909        | LeanWorkerError::Kill { .. }
1910        | LeanWorkerError::WaitTimeout { .. }
1911        | LeanWorkerError::ShutdownInProgress { .. }) => LeanWorkerBootstrapCheck::error(
1912            LeanWorkerBootstrapDiagnosticCode::WorkerStartupFailed,
1913            "worker bootstrap",
1914            other.to_string(),
1915            "run the bootstrap check in a deployment environment and rebuild the worker child or capability artifact",
1916        ),
1917    }
1918}
1919
1920fn format_paths(paths: &[PathBuf]) -> String {
1921    if paths.is_empty() {
1922        return "<none>".to_owned();
1923    }
1924    paths
1925        .iter()
1926        .map(|path| path.display().to_string())
1927        .collect::<Vec<_>>()
1928        .join(", ")
1929}
1930
1931fn bound_bootstrap_text(mut text: String) -> String {
1932    const LIMIT: usize = 1_024;
1933    if text.len() <= LIMIT {
1934        return text;
1935    }
1936    while !text.is_char_boundary(LIMIT) {
1937        text.pop();
1938    }
1939    text.truncate(LIMIT);
1940    text.push_str("...");
1941    text
1942}
1943
1944fn candidate_sibling_worker_paths(executable_name: &str) -> Vec<PathBuf> {
1945    let mut tried = Vec::new();
1946    if let Ok(current_exe) = env::current_exe() {
1947        if let Some(dir) = current_exe.parent() {
1948            tried.push(dir.join(executable_name));
1949        }
1950        if let Some(profile_dir) = current_exe.parent().and_then(Path::parent) {
1951            tried.push(profile_dir.join(executable_name));
1952        }
1953    }
1954    tried
1955}
1956
1957fn with_exe_suffix(mut executable_name: String) -> String {
1958    if !env::consts::EXE_SUFFIX.is_empty() && !executable_name.ends_with(env::consts::EXE_SUFFIX) {
1959        executable_name.push_str(env::consts::EXE_SUFFIX);
1960    }
1961    executable_name
1962}
1963
1964fn infer_lake_project_root_from_dylib(dylib_path: &Path) -> Result<PathBuf, LeanWorkerError> {
1965    let lib_dir = dylib_path.parent();
1966    let build_dir = lib_dir.and_then(Path::parent);
1967    let lake_dir = build_dir.and_then(Path::parent);
1968    let project_root = lake_dir.and_then(Path::parent);
1969    match (lib_dir, build_dir, lake_dir, project_root) {
1970        (Some(lib), Some(build), Some(lake), Some(root))
1971            if lib.file_name().is_some_and(|name| name == "lib")
1972                && build.file_name().is_some_and(|name| name == "build")
1973                && lake.file_name().is_some_and(|name| name == ".lake") =>
1974        {
1975            Ok(root.to_path_buf())
1976        }
1977        _ => Err(LeanWorkerError::Setup {
1978            message: format!(
1979                "built capability dylib '{}' is not under a standard .lake/build/lib directory",
1980                dylib_path.display()
1981            ),
1982        }),
1983    }
1984}
1985
1986fn normalize_import_workspace_root(path: PathBuf) -> PathBuf {
1987    std::fs::canonicalize(&path).unwrap_or(path)
1988}
1989
1990/// Build `lean-rs-worker-child` from source as an in-tree developer
1991/// convenience for the workspace's own examples, tests, and profiling harness.
1992///
1993/// `CARGO_MANIFEST_DIR` is baked at compile time of this crate, so the derived
1994/// `workspace` only contains `crates/lean-rs-worker-child/Cargo.toml` when this
1995/// crate is being built *as part of the `lean-rs` source tree*. For a
1996/// downstream that depends on the published crate, `CARGO_MANIFEST_DIR` points
1997/// into the registry cache, the guard below fails, and this returns `None`
1998/// without side effects. Downstreams must therefore provision their own worker
1999/// child (see [`LeanWorkerChild`]).
2000fn try_build_workspace_worker_child(executable_name: &str, tried: &mut Vec<PathBuf>) -> Option<PathBuf> {
2001    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2002    let workspace = manifest_dir.parent()?.parent()?;
2003    if !workspace
2004        .join("crates")
2005        .join("lean-rs-worker-child")
2006        .join("Cargo.toml")
2007        .is_file()
2008    {
2009        return None;
2010    }
2011
2012    let debug = workspace.join("target").join("debug").join(executable_name);
2013    let release = workspace.join("target").join("release").join(executable_name);
2014    tried.push(debug.clone());
2015    tried.push(release.clone());
2016    if debug.is_file() {
2017        return Some(debug);
2018    }
2019    if release.is_file() {
2020        return Some(release);
2021    }
2022
2023    let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
2024    let status = Command::new(cargo)
2025        .current_dir(workspace)
2026        .args(["build", "-p", "lean-rs-worker-child", "--bin", "lean-rs-worker-child"])
2027        .status()
2028        .ok()?;
2029    if !status.success() {
2030        return None;
2031    }
2032    debug.is_file().then_some(debug)
2033}
2034
2035fn dedup_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
2036    let mut unique = Vec::new();
2037    for path in paths {
2038        if !unique.iter().any(|existing| existing == path) {
2039            unique.push(path.clone());
2040        }
2041    }
2042    unique
2043}
2044
2045#[cfg(test)]
2046#[allow(clippy::expect_used, clippy::panic)]
2047mod tests {
2048    use super::{
2049        LeanWorkerCapabilityBuilder, LeanWorkerChild, LeanWorkerModuleCacheLimits, apply_module_cache_limits,
2050        apply_runtime_limits,
2051    };
2052    use crate::supervisor::LeanWorkerConfig;
2053    use lean_rs_worker_protocol::types::LeanWorkerSessionImportProfile;
2054    use lean_toolchain::LeanBuiltCapability;
2055    use std::path::PathBuf;
2056
2057    fn workspace_root() -> PathBuf {
2058        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2059        manifest_dir
2060            .parent()
2061            .and_then(std::path::Path::parent)
2062            .expect("crates/<name> lives two directories below the workspace root")
2063            .to_path_buf()
2064    }
2065
2066    fn interop_root() -> PathBuf {
2067        workspace_root().join("fixtures").join("interop-shims")
2068    }
2069
2070    fn capability_builder() -> LeanWorkerCapabilityBuilder {
2071        LeanWorkerCapabilityBuilder::new(
2072            interop_root(),
2073            "lean_rs_interop_consumer",
2074            "LeanRsInteropConsumer",
2075            ["LeanRsInteropConsumer.Callback"],
2076        )
2077    }
2078
2079    #[test]
2080    fn import_workspace_root_unset_matches_capability_project_root() {
2081        let key = capability_builder().session_key();
2082        assert_eq!(key.project_root(), interop_root().as_path());
2083        assert_eq!(key.import_workspace_root(), interop_root().as_path());
2084    }
2085
2086    #[test]
2087    fn project_root_is_canonicalized_for_session_reuse() {
2088        assert_eq!(
2089            capability_builder().session_key(),
2090            LeanWorkerCapabilityBuilder::new(
2091                interop_root().join("."),
2092                "lean_rs_interop_consumer",
2093                "LeanRsInteropConsumer",
2094                ["LeanRsInteropConsumer.Callback"],
2095            )
2096            .session_key(),
2097        );
2098    }
2099
2100    #[test]
2101    fn explicit_import_workspace_root_matching_capability_root_preserves_key() {
2102        assert_eq!(
2103            capability_builder().session_key(),
2104            capability_builder().import_workspace_root(interop_root()).session_key()
2105        );
2106    }
2107
2108    #[test]
2109    fn import_workspace_root_is_canonicalized_for_session_reuse() {
2110        assert_eq!(
2111            capability_builder().import_workspace_root(interop_root()).session_key(),
2112            capability_builder()
2113                .import_workspace_root(interop_root().join("."))
2114                .session_key(),
2115        );
2116    }
2117
2118    #[test]
2119    fn import_workspace_root_participates_in_session_key() {
2120        assert_ne!(
2121            capability_builder().session_key(),
2122            capability_builder()
2123                .import_workspace_root(workspace_root())
2124                .session_key(),
2125        );
2126    }
2127
2128    #[test]
2129    fn import_profile_participates_in_session_key() {
2130        assert_ne!(
2131            capability_builder().session_key(),
2132            capability_builder()
2133                .import_profile(LeanWorkerSessionImportProfile::FullPrivateCompat)
2134                .session_key(),
2135        );
2136    }
2137
2138    #[test]
2139    fn built_manifest_path_participates_in_session_key() {
2140        let manifest_dir = std::env::temp_dir().join(format!("lean-rs-worker-manifest-key-{}", std::process::id()));
2141        std::fs::create_dir_all(&manifest_dir).expect("manifest temp dir");
2142        let dylib = interop_root()
2143            .join(".lake")
2144            .join("build")
2145            .join("lib")
2146            .join(if cfg!(target_os = "macos") {
2147                "liblean__rs__interop__consumer_LeanRsInteropConsumer.dylib"
2148            } else {
2149                "liblean__rs__interop__consumer_LeanRsInteropConsumer.so"
2150            });
2151        let manifest_a = manifest_dir.join("a.json");
2152        let manifest_b = manifest_dir.join("b.json");
2153        for manifest in [&manifest_a, &manifest_b] {
2154            std::fs::write(
2155                manifest,
2156                format!(
2157                    r#"{{"schema_version":2,"primary_dylib":{},"package":"lean_rs_interop_consumer","module":"LeanRsInteropConsumer"}}"#,
2158                    serde_json::to_string(&dylib).expect("dylib path json")
2159                ),
2160            )
2161            .expect("write manifest");
2162        }
2163
2164        let key_a = LeanWorkerCapabilityBuilder::from_built_capability(
2165            &LeanBuiltCapability::manifest_path(&manifest_a),
2166            ["LeanRsInteropConsumer.Callback"],
2167        )
2168        .expect("manifest A accepted")
2169        .session_key();
2170        let key_a_dot = LeanWorkerCapabilityBuilder::from_built_capability(
2171            &LeanBuiltCapability::manifest_path(manifest_dir.join(".").join("a.json")),
2172            ["LeanRsInteropConsumer.Callback"],
2173        )
2174        .expect("canonical-equivalent manifest accepted")
2175        .session_key();
2176        let key_b = LeanWorkerCapabilityBuilder::from_built_capability(
2177            &LeanBuiltCapability::manifest_path(&manifest_b),
2178            ["LeanRsInteropConsumer.Callback"],
2179        )
2180        .expect("manifest B accepted")
2181        .session_key();
2182
2183        assert_eq!(key_a, key_a_dot);
2184        assert_ne!(key_a, key_b);
2185        drop(std::fs::remove_dir_all(manifest_dir));
2186    }
2187
2188    #[test]
2189    fn for_toolchain_carries_sysroot_through_resolve() {
2190        let sysroot = PathBuf::from("/opt/some/lean/prefix");
2191        let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2192        let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2193        assert_eq!(resolved, sysroot);
2194    }
2195
2196    #[test]
2197    fn lean_sysroot_setter_overrides_default() {
2198        let sysroot = PathBuf::from("/opt/override/lean");
2199        let child = LeanWorkerChild::path("/opt/worker").lean_sysroot(&sysroot);
2200        let resolved = child.resolve_lean_sysroot().expect("explicit sysroot resolves");
2201        assert_eq!(resolved, sysroot);
2202    }
2203
2204    #[test]
2205    fn explicit_sysroot_bypasses_discovery_even_when_path_is_nonexistent() {
2206        // The supervisor only sets `LEAN_SYSROOT`; it does not validate that
2207        // the path exists. Validation is the spawned child's responsibility
2208        // (an invalid sysroot manifests as a typed handshake/abort error
2209        // carrying the child's bootstrap stderr).
2210        let sysroot = PathBuf::from("/definitely/not/a/real/sysroot");
2211        let child = LeanWorkerChild::for_toolchain("/opt/worker", &sysroot);
2212        let resolved = child
2213            .resolve_lean_sysroot()
2214            .expect("explicit sysroot resolves without filesystem checks");
2215        assert_eq!(resolved, sysroot);
2216    }
2217
2218    #[test]
2219    fn module_cache_limits_map_to_typed_child_policy_env() {
2220        let limits = LeanWorkerModuleCacheLimits::default()
2221            .max_entries(7)
2222            .ttl(std::time::Duration::from_millis(250))
2223            .max_bytes(4096)
2224            .rss_guard_kib(8192);
2225        let config = apply_module_cache_limits(LeanWorkerConfig::new("/opt/worker"), &limits);
2226        let env = config.env_overrides();
2227        assert!(
2228            env.iter()
2229                .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_ENTRIES" && v == "7")
2230        );
2231        assert!(
2232            env.iter()
2233                .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_TTL_MILLIS" && v == "250")
2234        );
2235        assert!(
2236            env.iter()
2237                .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_MAX_BYTES" && v == "4096")
2238        );
2239        assert!(
2240            env.iter()
2241                .any(|(k, v)| k == "LEAN_RS_MODULE_CACHE_RSS_GUARD_KIB" && v == "8192")
2242        );
2243    }
2244
2245    #[test]
2246    fn runtime_limits_map_to_typed_child_env() {
2247        let config = apply_runtime_limits(LeanWorkerConfig::new("/opt/worker"), Some(1), Some(16_777_216));
2248        let env = config.env_overrides();
2249        assert!(env.iter().any(|(k, v)| k == "LEAN_RS_NUM_THREADS" && v == "1"));
2250        assert!(
2251            env.iter()
2252                .any(|(k, v)| k == "LEAN_RS_LEAN_MAX_MEMORY_KIB" && v == "16777216")
2253        );
2254    }
2255
2256    #[test]
2257    fn runtime_limits_are_absent_when_unset() {
2258        let config = apply_runtime_limits(LeanWorkerConfig::new("/opt/worker"), None, None);
2259        let env = config.env_overrides();
2260        assert!(!env.iter().any(|(k, _)| k == "LEAN_RS_NUM_THREADS"));
2261        assert!(!env.iter().any(|(k, _)| k == "LEAN_RS_LEAN_MAX_MEMORY_KIB"));
2262    }
2263}