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