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