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