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