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