Skip to main content

rch_common/
lib.rs

1//! Remote Compilation Helper - Common Library
2//!
3//! Shared types, patterns, and utilities used by rch, rchd, and rch-wkr.
4
5// Use deny instead of forbid to allow specific overrides for env var manipulation
6// in tests and profile defaults (env::set_var/remove_var are unsafe in Rust 2024)
7#![deny(unsafe_code)]
8
9use std::sync::OnceLock;
10
11mod remote_scoped_launch;
12/// Shared Linux launch and cancellation protocol; callers select their receipt.
13pub use remote_scoped_launch::REMOTE_PROCESS_IDENTITY_SCRIPT;
14
15pub mod admission_recommendation;
16pub mod admission_rejection;
17pub mod admit_preflight;
18pub mod api;
19pub mod artifact_cost;
20pub mod artifact_pattern;
21pub mod artifact_verify;
22pub mod binary_hash;
23pub mod bypass_record;
24pub mod bypass_recovery;
25pub mod capability_probe;
26pub mod cargo_path_deps;
27pub mod classifier_drift;
28pub mod closure_explain;
29pub mod config;
30pub mod dependency_closure_planner;
31pub mod discovery;
32pub mod disk_pressure_report;
33pub mod disk_reclaim;
34pub mod e2e;
35pub mod errors;
36pub mod exec_misuse;
37pub mod exec_policy;
38pub mod exec_response;
39pub mod fleet_diff;
40pub mod fleet_provenance;
41pub mod fleet_smoke_profile;
42pub mod fleet_status;
43pub mod force_resync;
44pub mod gc_roots;
45pub mod hooks;
46pub mod incident;
47pub mod incident_ledger;
48pub mod isolated_staging;
49pub mod job_identity;
50pub mod job_recovery;
51pub mod log_retention;
52pub mod logging;
53pub mod mock;
54pub mod mock_worker;
55pub mod orphan_quarantine;
56pub mod path_topology;
57pub mod patterns;
58#[cfg(test)]
59mod patterns_security_test;
60pub mod placement;
61pub mod pnpm_workspace;
62pub mod pooled_target_key;
63pub mod process_identity;
64pub mod proof_handoff;
65pub mod proof_intent;
66pub mod proof_policy;
67pub mod proof_replay;
68#[cfg(test)]
69mod proptest_tests;
70pub mod protocol;
71pub mod queue_contract;
72pub mod readiness;
73pub mod redaction;
74pub mod remediation_config;
75pub mod remediation_view;
76#[cfg(unix)]
77pub mod remote_compilation;
78#[cfg(unix)]
79pub mod remote_verification;
80pub mod repo_updater_contract;
81pub mod rsync_flavor;
82pub mod schema_versions;
83#[cfg(unix)]
84pub mod ssh;
85#[cfg(all(test, unix))]
86mod ssh_timeout_test;
87pub mod ssh_utils;
88pub mod stale_target_reap;
89pub mod storm_control;
90pub mod telemetry_explain;
91pub mod telemetry_freshness;
92pub mod test_change;
93pub mod testing;
94pub mod tool_requirement;
95pub mod toolchain;
96pub mod transfer_hardening;
97pub mod types;
98pub mod ui;
99pub mod util;
100pub mod worker_config_validation;
101pub mod worker_facts;
102
103pub const BUILD_COMMIT_ENV_VARS: &[&str] = &[
104    "RCH_GIT_COMMIT",
105    "VERGEN_GIT_SHA",
106    "GIT_COMMIT",
107    "GITHUB_SHA",
108];
109
110/// The validated build revision stamped into this binary, if any.
111///
112/// `build.rs` validates the commit and blanks the ambient aliases; this
113/// re-applies the same grammar so a value the build script rejected can never
114/// be displayed as a build revision (issue #76).
115pub fn build_commit() -> Option<&'static str> {
116    first_build_commit([
117        option_env!("RCH_GIT_COMMIT"),
118        option_env!("VERGEN_GIT_SHA"),
119        option_env!("GIT_COMMIT"),
120        option_env!("GITHUB_SHA"),
121    ])
122}
123
124fn first_build_commit<'a>(
125    candidates: impl IntoIterator<Item = Option<&'a str>>,
126) -> Option<&'a str> {
127    candidates
128        .into_iter()
129        .flatten()
130        .map(str::trim)
131        .find(|value| is_build_commit_stamp(value))
132}
133
134/// Same grammar as `rch-common/build.rs`: a 7..=64 hex commit, optionally
135/// suffixed `-dirty` or `-overlay-<64 hex fingerprint>` (build-source stamps).
136fn is_build_commit_stamp(value: &str) -> bool {
137    fn is_commit_hash(value: &str) -> bool {
138        (7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
139    }
140    is_commit_hash(value)
141        || value.strip_suffix("-dirty").is_some_and(is_commit_hash)
142        || value
143            .split_once("-overlay-")
144            .is_some_and(|(commit, fingerprint)| {
145                is_commit_hash(commit)
146                    && fingerprint.len() == 64
147                    && fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit())
148            })
149}
150
151pub fn build_version_value() -> String {
152    build_version_value_with_commit(env!("CARGO_PKG_VERSION"), build_commit())
153}
154
155pub fn build_version_value_static() -> &'static str {
156    static VERSION_VALUE: OnceLock<String> = OnceLock::new();
157
158    VERSION_VALUE.get_or_init(build_version_value).as_str()
159}
160
161pub fn build_version_value_with_commit(package_version: &str, commit: Option<&str>) -> String {
162    let mut value = package_version.to_string();
163
164    if let Some(commit) = commit.map(str::trim).filter(|value| !value.is_empty()) {
165        value.push_str(" (commit ");
166        let (revision, suffix) = commit
167            .split_once('-')
168            .map_or((commit, None), |(revision, suffix)| {
169                (revision, Some(suffix))
170            });
171        value.push_str(short_commit(revision));
172        if let Some(suffix) = suffix {
173            value.push('-');
174            value.push_str(suffix);
175        }
176        value.push(')');
177    }
178
179    value
180}
181
182fn short_commit(commit: &str) -> &str {
183    commit
184        .char_indices()
185        .nth(12)
186        .map_or(commit, |(index, _)| &commit[..index])
187}
188
189pub use artifact_cost::{
190    ArtifactCostReport, ArtifactPhaseTimings, GlobAdvice, RetrievalMode, assess_glob_expansion,
191    compute_artifact_cost, is_broad_recursive_glob,
192};
193pub use artifact_pattern::{
194    ARTIFACT_OUTPUT_ROOTS, ArtifactPatternRewrite, ArtifactRetrievalDiagnostics,
195    artifact_dest_is_safe, rewrite_artifact_pattern,
196};
197pub use artifact_verify::{
198    ArtifactManifest, FileHash, VerificationFailure, VerificationResult, compute_file_hash,
199    create_manifest, verify_artifacts,
200};
201pub use binary_hash::{
202    BinaryHashResult, binaries_equivalent, binary_contains_marker, compute_binary_hash,
203};
204pub use bypass_record::{
205    AutoRejoinCriteria, BypassBackoff, BypassFailureClass, BypassRecord, BypassRecordStore,
206    BypassState, DisabledMigration, DisabledWorkerSnapshot, MAX_DIAGNOSTIC_CHARS,
207    bypass_record_schema, bypass_record_schema_version, classify_disable_reason,
208    default_bypass_record_path, migrate_disabled_worker, truncate_diagnostic,
209};
210pub use bypass_recovery::{
211    CanaryDecision, CanaryOutcome, ProbeDecision, RecoveryProbe, decide_canary, decide_probe,
212};
213pub use cargo_path_deps::{
214    CargoPathDependencyEdge, CargoPathDependencyError, CargoPathDependencyErrorKind,
215    CargoPathDependencyGraph, CargoPathDependencyPackage, CargoPathMaterializationClosure,
216    resolve_cargo_path_dependency_graph, resolve_cargo_path_dependency_graph_with_policy,
217    resolve_cargo_path_materialization_closure,
218    resolve_cargo_path_materialization_closure_with_policy,
219};
220pub use closure_explain::{
221    ClosureExplainEntry, ClosureExplainReport, RootConvergence, RootSyncOutcome, derive_outcome,
222    explain_closure,
223};
224pub use dependency_closure_planner::{
225    DependencyClosurePlan, DependencyClosurePlanState, DependencyPlanIssue, DependencyRiskClass,
226    DependencySyncAction, DependencySyncMetadata, DependencySyncReason,
227    build_dependency_closure_plan, build_dependency_closure_plan_with_policy,
228    plan_dependency_closure_from_graph,
229};
230pub use force_resync::{
231    ForceResyncPlan, ForceResyncReport, InvalidationAction, RefusedInvalidation, ResyncOutcome,
232    StaleRoot, apply_force_resync, is_safe_invalidation_target, plan_force_resync,
233};
234pub use gc_roots::{
235    RCH_CARGO_CACHE_PREFIX, RCH_CARGO_HOME_BASE_VAR, RCH_CARGO_HOME_PREFIX,
236    remote_cargo_cache_expr, remote_cargo_home_base_prelude, remote_cargo_home_expr,
237};
238pub use logging::{LogConfig, LogFormat, LoggingGuards, init_logging, init_logging_with_layer};
239pub use mock_worker::MockWorkerServer;
240pub use path_topology::{
241    DEFAULT_ALIAS_PROJECT_ROOT, DEFAULT_CANONICAL_PROJECT_ROOT, NormalizationDecision,
242    NormalizedProjectPath, PathNormalizationError, PathNormalizationErrorKind, PathTopologyPolicy,
243    normalize_project_path, normalize_project_path_with_policy,
244};
245pub use patterns::{
246    Classification, ClassificationDetails, ClassificationTier, CompilationKind, TierDecision,
247    classify_command, classify_command_detailed, compound_local_compilation_segments,
248    declined_compilation_due_to_structure, split_shell_commands,
249};
250pub use protocol::{HookInput, HookOutput, ToolInput};
251pub use queue_contract::{
252    AdmissionState, QueueContractOutcome, QueueContractResponse, QueueOptions, StartState,
253    WaitResult, resolve_queue_contract,
254};
255pub use repo_updater_contract::{
256    MockRepoUpdaterAdapter, REPO_UPDATER_ALIAS_PROJECTS_ROOT, REPO_UPDATER_CANONICAL_PROJECTS_ROOT,
257    REPO_UPDATER_CONTRACT_SCHEMA_VERSION, REPO_UPDATER_DEFAULT_BINARY,
258    REPO_UPDATER_MIN_SUPPORTED_VERSION, RepoUpdaterAdapter, RepoUpdaterAdapterCommand,
259    RepoUpdaterAdapterContract, RepoUpdaterAdapterRequest, RepoUpdaterAdapterResponse,
260    RepoUpdaterFailure, RepoUpdaterFailureKind, RepoUpdaterFallbackMode,
261    RepoUpdaterIdempotencyGuarantee, RepoUpdaterJsonEnvelope, RepoUpdaterOutputFormat,
262    RepoUpdaterResponseStatus, RepoUpdaterVersionCompatibility, RepoUpdaterVersionPolicy,
263    build_invocation, classify_exit_code, evaluate_version_compatibility,
264    map_failure_kind_to_error_code, repo_updater_envelope_schema, repo_updater_request_schema,
265    repo_updater_response_schema,
266};
267// Platform-independent SSH utilities (available everywhere)
268pub use ssh_utils::{
269    CommandResult, EnvPrefix, build_env_prefix, is_retryable_transport_error,
270    is_retryable_transport_error_text, shell_escape_value,
271};
272// Unix-only SSH client (uses openssh crate)
273#[cfg(unix)]
274pub use ssh::{KnownHostsPolicy, SshClient, SshOptions, SshPool};
275pub use test_change::{TestChangeGuard, TestCodeChange};
276pub use toolchain::{ToolchainInfo, wrap_command_with_color, wrap_command_with_toolchain};
277pub use transfer_hardening::{
278    FirstFailure, RetryDecision, RsyncFailureClass, classify_rsync_outcome, is_ephemeral_path,
279};
280pub use types::{
281    AffinityConfig, ApiConfig, BoxRole, BuildCancellationMetadata, BuildCancellationWorkerHealth,
282    BuildHeartbeatPhase, BuildHeartbeatRequest, BuildLocation, BuildRecord, BuildStats,
283    CircuitBreakerConfig, CircuitState, CircuitStats, ColorMode, CommandPriority,
284    CommandTimingBreakdown, CompilationConfig, CompilationMetrics, CompilationTimer,
285    CompilationTimingBreakdown, DashboardConfig, DoctorConfig, DoctorWebhookEndpoint,
286    DoctorWebhookFormat, DoctorWebhooksConfig, EnvironmentConfig, ExecutionConfig, FairnessConfig,
287    FleetConfig, GeneralConfig, MetricsAggregator, OS_TAG_PREFIX, OutputConfig, OutputVisibility,
288    PathTopologyConfig, RchConfig, ReleaseRequest, RemoteBuildJobs, RequiredRuntime, RetryConfig,
289    SELECTION_RESPONSE_PROTOCOL_VERSION, SavedTimeStats, SelectedWorker, SelectionConfig,
290    SelectionDiagnostics, SelectionReason, SelectionRequest, SelectionResponse, SelectionStrategy,
291    SelectionWeightConfig, SelfHealingConfig, SelfHealingLogLevel, SelfTestConfig,
292    SelfTestFailureAction, SelfTestWorkers, TransferConfig, WorkerCapabilities, WorkerConfig,
293    WorkerId, WorkerSelectionDiagnostic, WorkerSelectionDiagnosticDecision, WorkerStatus,
294    declared_os, default_socket_path, os_tag, validate_remote_base,
295    x86_64_microarch_level_from_flags,
296};
297
298// Testing module re-exports
299pub use testing::{TestLogEntry, TestLogger, TestPhase, TestResult};
300
301// Config module re-exports
302pub use config::{
303    ConfigSource, ConfigValueSource, ConfigWarning, EnvError, EnvParser, Profile, Severity,
304    Sourced, validate_config,
305};
306
307// Discovery module re-exports
308pub use discovery::{
309    DiscoveredHost, DiscoverySource, discover_all, parse_shell_aliases,
310    parse_shell_aliases_content, parse_ssh_config, parse_ssh_config_content,
311};
312
313// UI module re-exports
314pub use ui::{
315    ErrorPanel, ErrorSeverity, Icons, IntoErrorPanel, OutputContext, RchTheme, ResultExt,
316    anyhow_to_json, anyhow_to_panel, display_anyhow_error, display_error, display_error_with_code,
317    error_to_json, error_to_panel,
318};
319
320// Incident schema re-exports
321pub use capability_probe::{
322    CapabilityRequirement, CapabilityVerdict, EligibilityVerdict, ProbeSpec, ProbedFacts,
323    WorkerLiveness, assess_admissibility, assess_worker_eligibility, build_capability_probe_script,
324    parse_capability_probe, remote_worker_binary_path,
325};
326pub use incident::{
327    ControlState, IncidentEvent, IncidentEventType, IncidentReasonCode, IncidentSource,
328    SelectedMode, incident_schema_version,
329};
330pub use incident_ledger::{
331    IncidentFilter, IncidentLedger, IncidentLedgerConfig, LedgerReadStats, default_ledger_path,
332};
333pub use placement::{
334    ControlDiagnostic, ControlDiagnosticLevel, ControlKind, PlacementControl, PlacementPlan,
335    QueuePolicy, RequestedWorkerFacts, RequestedWorkerOutcome, RequestedWorkerStatus,
336    StrictRemotePolicy, TargetDirPolicy, VisibilityMode, evaluate_requested_worker,
337    placement_controls, resolve_placement,
338};
339pub use proof_intent::{
340    ProofIntent, ProofIntentStore, ReplayConstraints, ReplayContext, ReplayDecision,
341    SourceFingerprint, StaleSourcePolicy, derive_intent_id, proof_intent_schema,
342    proof_intent_schema_version, validate_replay,
343};
344pub use telemetry_explain::{
345    ProbeOutcome, TelemetrySignals, TelemetryUnavailabilityReason, WhyUnhealthy,
346    explain_unavailability,
347};
348pub use telemetry_freshness::{
349    FreshnessAssessment, FreshnessInputs, FreshnessVerdict, assess as assess_telemetry_freshness,
350};
351pub use worker_config_validation::{
352    ConfigDriftFinding, ConfigDriftKind, LiveHostObservation, validate_worker_config,
353};
354pub use worker_facts::{
355    DiskFacts, DiskRootFacts, HostFacts, RuntimeFacts, RustFacts, UserFacts, WorkerBinaryFacts,
356    WorkerFacts, derive_target_triple, worker_facts_schema, worker_facts_schema_version,
357};
358
359// Errors module re-exports
360pub use errors::{
361    CodeExplanation, CodeNamespace, ErrorCategory, ErrorCode, ErrorEntry, ReliabilityCategoryKind,
362    ReliabilityReasonCode, RunbookEntry,
363};
364
365// Schema-version registry re-exports
366pub use schema_versions::current_version as schema_version;
367pub use schema_versions::{ALL_COMPONENTS as SCHEMA_VERSION_COMPONENTS, SchemaComponent};
368
369// API module re-exports (unified API types for CLI and daemon)
370pub use api::{API_VERSION, ApiError, ApiResponse, ErrorContext, LegacyErrorCode};
371
372// Hooks module re-exports (daemon self-healing)
373pub use hooks::{HookResult, is_claude_code_installed, verify_and_install_claude_code_hook};
374
375#[cfg(test)]
376mod build_version_tests {
377    use super::{BUILD_COMMIT_ENV_VARS, build_version_value_with_commit, first_build_commit};
378
379    #[test]
380    fn build_commit_skips_values_the_build_script_rejects() {
381        // Issue #76: an alias build.rs refused must not surface in --version.
382        assert_eq!(
383            first_build_commit([None, None, Some("not-a-commit"), None]),
384            None
385        );
386        let hash = "0123456789abcdef0123456789abcdef01234567";
387        assert_eq!(
388            first_build_commit([Some(""), None, Some("v2.0.0-3-gdeadbee"), Some(hash)]),
389            Some(hash)
390        );
391        assert_eq!(first_build_commit([Some(" abc1234 ")]), Some("abc1234"));
392        assert_eq!(first_build_commit([Some("abc123")]), None, "too short");
393        let dirty = format!("{hash}-dirty");
394        assert_eq!(
395            first_build_commit([Some(dirty.as_str())]),
396            Some(dirty.as_str())
397        );
398        let overlay = format!("{hash}-overlay-{}", "b".repeat(64));
399        assert_eq!(
400            first_build_commit([Some(overlay.as_str())]),
401            Some(overlay.as_str())
402        );
403        let short_overlay = format!("{hash}-overlay-{}", "b".repeat(63));
404        assert_eq!(first_build_commit([Some(short_overlay.as_str())]), None);
405    }
406
407    #[test]
408    fn build_version_value_omits_missing_commit() {
409        assert_eq!(build_version_value_with_commit("1.0.24", None), "1.0.24");
410    }
411
412    #[test]
413    fn build_version_value_trims_and_shortens_commit() {
414        assert_eq!(
415            build_version_value_with_commit(
416                "1.0.24",
417                Some(" 2fa1249a18dfd05ae5e319e8d10bfd3c9ea1af55 "),
418            ),
419            "1.0.24 (commit 2fa1249a18df)"
420        );
421    }
422
423    #[test]
424    fn build_version_value_ignores_empty_commit() {
425        assert_eq!(
426            build_version_value_with_commit("1.0.24", Some("  ")),
427            "1.0.24"
428        );
429    }
430
431    #[test]
432    fn build_source_stamp_version_preserves_nonclean_suffixes() {
433        let hash = "a".repeat(40);
434        for suffix in ["dirty".to_string(), format!("overlay-{}", "b".repeat(64))] {
435            assert_eq!(
436                build_version_value_with_commit("2.0.0", Some(&format!("{hash}-{suffix}"))),
437                format!("2.0.0 (commit {}-{suffix})", "a".repeat(12))
438            );
439        }
440    }
441
442    #[test]
443    fn build_commit_env_var_order_is_documented() {
444        assert_eq!(
445            BUILD_COMMIT_ENV_VARS,
446            &[
447                "RCH_GIT_COMMIT",
448                "VERGEN_GIT_SHA",
449                "GIT_COMMIT",
450                "GITHUB_SHA"
451            ]
452        );
453    }
454}