Skip to main content

talos_plugin/
resolution.rs

1//! Explicit, bounded on-demand Bundle resolution.
2
3use std::path::{Path, PathBuf};
4use std::time::{Duration, Instant};
5
6use crate::{
7    BundleManifest, CompatibleManifest, InstallError, install_bundle_with_guard,
8    parse_compatible_manifest,
9};
10
11/// User consent required before resolving an optional Bundle.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum ResolutionConsent {
14    /// The user explicitly approved this exact request.
15    Granted,
16    /// No resolution is allowed.
17    Denied,
18}
19
20/// Stable identity of the capability being resolved.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct ResolutionRequest {
23    /// Capability identity requested by the consumer.
24    pub capability: String,
25    /// Exact Bundle name expected from the source.
26    pub bundle_name: String,
27    /// Exact Bundle version expected from the source.
28    pub bundle_version: String,
29}
30
31/// Bounded controls applied before any Bundle files are copied.
32#[derive(Clone, Copy, Debug)]
33pub struct ResolutionLimits {
34    /// Maximum time allowed for this resolution operation.
35    pub timeout: Duration,
36}
37
38impl Default for ResolutionLimits {
39    fn default() -> Self {
40        Self {
41            timeout: Duration::from_secs(30),
42        }
43    }
44}
45
46/// Explicit resolution outcome. Installation does not activate executable content.
47#[derive(Debug)]
48pub struct ResolutionResult {
49    /// Validated manifest installed into the inactive destination.
50    pub manifest: BundleManifest,
51    /// Destination that remains inactive until normal lifecycle admission.
52    pub destination: PathBuf,
53}
54
55/// Errors are deliberately fail-closed and retain the reason for audit output.
56#[derive(Debug, thiserror::Error)]
57pub enum ResolutionError {
58    /// Consent was not granted for the exact request.
59    #[error("explicit consent is required")]
60    ConsentDenied,
61    /// Request identity did not match the verified manifest.
62    #[error("resolved Bundle identity does not match the request")]
63    IdentityMismatch,
64    /// The operation exceeded its bound or was cancelled.
65    #[error("resolution cancelled or timed out")]
66    Cancelled,
67    /// Bundle verification or installation failed.
68    #[error("Bundle installation failed: {0}")]
69    Install(#[from] InstallError),
70}
71
72/// Resolve a manually supplied/offline Bundle only after exact user consent.
73///
74/// This function intentionally accepts a local source path: network acquisition is an outer
75/// policy owned by a future connector and cannot be implied by this resolver. The Bundle is
76/// validated and copied, but never activated or registered here.
77pub fn resolve_verified_bundle(
78    request: &ResolutionRequest,
79    source: &Path,
80    destination: &Path,
81    consent: ResolutionConsent,
82    limits: ResolutionLimits,
83    cancelled: impl Fn() -> bool,
84) -> Result<ResolutionResult, ResolutionError> {
85    if consent != ResolutionConsent::Granted || cancelled() {
86        return Err(if consent == ResolutionConsent::Granted {
87            ResolutionError::Cancelled
88        } else {
89            ResolutionError::ConsentDenied
90        });
91    }
92    let started = Instant::now();
93    let manifest_text =
94        std::fs::read_to_string(source.join("manifest.toml")).map_err(InstallError::Io)?;
95    let expected =
96        match parse_compatible_manifest(&manifest_text).map_err(InstallError::Manifest)? {
97            CompatibleManifest::Bundle(bundle) => bundle,
98            CompatibleManifest::Legacy(_) => {
99                return Err(ResolutionError::Install(InstallError::LegacyManifest));
100            }
101        };
102    let provider_matches = expected
103        .language_provider
104        .as_ref()
105        .map(|provider| format!("language.{}", provider.language) == request.capability)
106        .unwrap_or(false);
107    if expected.bundle.name != request.bundle_name
108        || expected.bundle.version != request.bundle_version
109        || !provider_matches
110    {
111        return Err(ResolutionError::IdentityMismatch);
112    }
113    let staging = destination.with_extension("resolution-staging");
114    let manifest = install_bundle_with_guard(source, &staging, || {
115        cancelled() || started.elapsed() > limits.timeout
116    })
117    .map_err(|error| match error {
118        InstallError::Io(io) if io.kind() == std::io::ErrorKind::Interrupted => {
119            ResolutionError::Cancelled
120        }
121        InstallError::Cancelled => ResolutionError::Cancelled,
122        other => ResolutionError::Install(other),
123    });
124    let manifest = match manifest {
125        Ok(manifest) => manifest,
126        Err(error) => {
127            let _ = std::fs::remove_dir_all(&staging);
128            return Err(error);
129        }
130    };
131    if started.elapsed() > limits.timeout || cancelled() {
132        let _ = std::fs::remove_dir_all(&staging);
133        return Err(ResolutionError::Cancelled);
134    }
135    debug_assert_eq!(manifest.bundle.name, request.bundle_name);
136    let backup = destination.with_extension("resolution-backup");
137    if backup.exists() {
138        std::fs::remove_dir_all(&backup).map_err(InstallError::Io)?;
139    }
140    if destination.exists() {
141        std::fs::rename(destination, &backup).map_err(InstallError::Io)?;
142    }
143    if let Err(error) = std::fs::rename(&staging, destination) {
144        if backup.exists() && !destination.exists() {
145            let _ = std::fs::rename(&backup, destination);
146        }
147        let _ = std::fs::remove_dir_all(&staging);
148        return Err(ResolutionError::Install(InstallError::Io(error)));
149    }
150    if backup.exists() {
151        std::fs::remove_dir_all(&backup).map_err(InstallError::Io)?;
152    }
153    Ok(ResolutionResult {
154        manifest,
155        destination: destination.to_path_buf(),
156    })
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::fs;
163
164    fn fixture(root: &Path) -> PathBuf {
165        let source = root.join("source");
166        fs::create_dir_all(&source).expect("test fixture operation");
167        fs::write(source.join("provider.wat"), "(module)").expect("test fixture operation");
168        fs::write(
169            source.join("manifest.toml"),
170            "schema_version=1\n[bundle]\nname=\"demo\"\nversion=\"1.0.0\"\ncarrier=\"wasm\"\nartifact=\"provider.wat\"\n[language_provider]\nlanguage=\"python\"\nartifact=\"provider.wat\"",
171        )
172        .expect("test fixture operation");
173        source
174    }
175
176    #[test]
177    fn denied_consent_does_not_write() {
178        let root = std::env::temp_dir().join(format!("talos-resolution-{}", std::process::id()));
179        let source = fixture(&root);
180        let destination = root.join("installed");
181        let request = ResolutionRequest {
182            capability: "language.python".into(),
183            bundle_name: "demo".into(),
184            bundle_version: "1.0.0".into(),
185        };
186        assert!(matches!(
187            resolve_verified_bundle(
188                &request,
189                &source,
190                &destination,
191                ResolutionConsent::Denied,
192                ResolutionLimits::default(),
193                || false
194            ),
195            Err(ResolutionError::ConsentDenied)
196        ));
197        assert!(!destination.exists());
198        let _ = fs::remove_dir_all(root);
199    }
200
201    #[test]
202    fn identity_mismatch_rolls_back_inactive_install() {
203        let root = std::env::temp_dir().join(format!("talos-resolution-id-{}", std::process::id()));
204        let source = fixture(&root);
205        let destination = root.join("installed");
206        let request = ResolutionRequest {
207            capability: "language.python".into(),
208            bundle_name: "other".into(),
209            bundle_version: "1.0.0".into(),
210        };
211        assert!(matches!(
212            resolve_verified_bundle(
213                &request,
214                &source,
215                &destination,
216                ResolutionConsent::Granted,
217                ResolutionLimits::default(),
218                || false
219            ),
220            Err(ResolutionError::IdentityMismatch)
221        ));
222        assert!(!destination.exists());
223        let _ = fs::remove_dir_all(root);
224    }
225
226    #[test]
227    fn cancelled_resolution_preserves_existing_install() {
228        let root =
229            std::env::temp_dir().join(format!("talos-resolution-cancel-{}", std::process::id()));
230        let source = fixture(&root);
231        let destination = root.join("installed");
232        fs::create_dir_all(&destination).expect("test fixture operation");
233        fs::write(destination.join("sentinel"), "old").expect("test fixture operation");
234        let request = ResolutionRequest {
235            capability: "language.python".into(),
236            bundle_name: "demo".into(),
237            bundle_version: "1.0.0".into(),
238        };
239        assert!(matches!(
240            resolve_verified_bundle(
241                &request,
242                &source,
243                &destination,
244                ResolutionConsent::Granted,
245                ResolutionLimits::default(),
246                || true
247            ),
248            Err(ResolutionError::Cancelled)
249        ));
250        assert_eq!(
251            fs::read_to_string(destination.join("sentinel")).expect("test fixture operation"),
252            "old"
253        );
254        let _ = fs::remove_dir_all(root);
255    }
256}