Skip to main content

a3s_use_core/
lib.rs

1use std::collections::BTreeMap;
2use std::ffi::OsString;
3use std::fmt;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8mod filesystem;
9pub mod okf;
10pub mod plugin;
11pub mod release;
12
13pub use filesystem::metadata_is_link_or_reparse_point;
14#[cfg(windows)]
15#[doc(hidden)]
16pub use filesystem::windows_extended_length_path;
17pub use okf::{
18    inspect_okf_bundle, inspect_okf_bundle_files, OkfBundleContract, OkfBundleDiagnostic,
19    OkfBundleFile, OkfBundleInspection, OkfBundleLimits, OkfCapabilityProjection,
20    OkfConceptSummary, OkfDiagnosticCode, OkfFormatVersion, OkfKnowledgeObservation,
21    OkfKnowledgeObservedState, OkfProjectionReceipt, OkfSelectedGeneration,
22    OKF_BUNDLE_CONTRACT_SCHEMA, OKF_CAPABILITY_PROJECTION_SCHEMA, OKF_KNOWLEDGE_OBSERVATION_SCHEMA,
23    OKF_PROJECTION_RECEIPT_SCHEMA,
24};
25pub use plugin::{
26    ArtifactRef, CapabilityConsumerExtension, CapabilityConsumerKind,
27    CapabilityConsumerNegotiation, CapabilityConsumerProfile, CapabilityDescriptionProof,
28    CapabilityDescriptor, CapabilityDescriptorKind, CapabilityGatewayCatalog,
29    CapabilityMcpTransport, CapabilityPublicationEvidence, CapabilityToolAnnotations,
30    CatalogArchive, CatalogAvailability, CatalogMcpTransport, CatalogPackage,
31    CatalogPlanningTarget, CatalogSurface, EndpointRef, ExecutablePlanningSurface,
32    FilesystemAccess, FilesystemPermission, FilesystemScope, HttpMethod, InstallationId,
33    InstallationKind, InstallationPackageSelection, InstallationRootSelection,
34    InstallationSnapshot, InstalledPluginPlanEvidence, InvocationRef, LockedPluginPackage,
35    LockedPluginPackageDependency, NetworkEgressPermission, PlanActor, PlanAuthority,
36    PlanEnforcementProfile, PlanPackageChangeKind, PlanPackageRole, PlanPolicyDecision,
37    PlanQualifiedSurfaceRef, PlanScope, PlanScopeKind, PlannedOkfSurfaceChange,
38    PlannedOperationImpact, PlannedPackageState, PlannedPackageTransition, PlannedPluginRelease,
39    PlannedProviderEvidence, PlannedSecretChange, PlannedSecretChangeKind, PlannedStateEvidence,
40    PlannedSurfaceChange, PlannedWorkspaceGrantChange, PlannedWorkspaceImpact, PlanningArtifactRef,
41    PlanningSurfaceActivation, PluginCatalogRecord, PluginDesiredState, PluginGrantConfirmation,
42    PluginHostApplyRequest, PluginHostApplyResult, PluginHostCancelRequest, PluginHostCancelResult,
43    PluginHostCancellationStatus, PluginHostCapabilities, PluginHostEnablementPlanRequest,
44    PluginHostEnablementPlanResult, PluginHostEnablementPlanStatus, PluginHostManager,
45    PluginHostObservationRequest, PluginHostObservationResult, PluginHostObservationStatus,
46    PluginHostOperationCancellability, PluginHostOperationObservationRequest,
47    PluginHostOperationObservationResult, PluginHostOperationPhase, PluginHostOperationProgress,
48    PluginHostOperationStatus, PluginHostOperationWatchRequest, PluginHostPackageState,
49    PluginHostPlanRequest, PluginHostPlanResult, PluginHostUnavailableReason, PluginManagedScope,
50    PluginManagerApplyPlanInput, PluginManagerInspectInput, PluginManagerInstallPlanInput,
51    PluginManagerListInstalledInput, PluginManagerPackageScopeInput, PluginManagerSearchInput,
52    PluginManagerToolAnnotations, PluginManagerToolDefinition, PluginManagerToolset,
53    PluginManagerUpgradePlanInput, PluginObservedState, PluginOperationAction,
54    PluginOperationConfirmation, PluginOperationPlan, PluginOperationPlanBinding,
55    PluginOperationPlanDraft, PluginOperationPlanEnvelope, PluginPackageDependency,
56    PluginPackageId, PluginPackageLock, PluginPackageLockHost, PluginPackageResolver,
57    PluginPermissionCeiling, PluginPlanSource, PluginPlanningBundle, PluginReleaseChannel,
58    PluginSurfaceKind, PluginSurfaceRef, PluginWorkspaceGrant, PluginWorkspaceGrantChangeSet,
59    PluginWorkspaceGrantProposal, PluginWorkspaceGrantSnapshot, ResolvedWorkspaceGrant,
60    ResolvedWorkspaceGrantChangeSet, ResourcePermissionCeiling, SurfaceChangeKind,
61    SurfacePermissionCeiling, ToolWorkloadClass, UiHttpPermission, VerifiedCatalogProvenance,
62    VerifiedPluginCatalogRecord, WorkspaceGrantAuthority, WorkspaceGrantEvidence,
63    WorkspaceGrantProposalAuthority, CAPABILITY_CONSUMER_NEGOTIATION_SCHEMA_V1,
64    CAPABILITY_CONSUMER_PROFILE_SCHEMA_V1, CAPABILITY_DESCRIPTION_PROOF_SCHEMA_V1,
65    CAPABILITY_DESCRIPTOR_SCHEMA_V1, CAPABILITY_GATEWAY_CATALOG_SCHEMA_V1,
66    INSTALLATION_SNAPSHOT_SCHEMA, INSTALLED_PLUGIN_PLAN_EVIDENCE_SCHEMA,
67    MAX_CAPABILITY_CONSUMER_EXTENSIONS, MAX_INSTALLATION_PACKAGES, MAX_INSTALLATION_ROOTS,
68    MAX_INSTALLATION_SNAPSHOT_BYTES, MAX_PLUGIN_CONTRACT_BYTES,
69    MAX_PLUGIN_HOST_OPERATION_WATCH_TIMEOUT_MS, MAX_PLUGIN_PACKAGE_DEPENDENCIES,
70    MAX_PLUGIN_PLAN_ITEMS, MAX_PLUGIN_RESOLUTION_CANDIDATES, PLUGIN_CATALOG_SCHEMA_V3,
71    PLUGIN_GRANT_CONFIRMATION_SCHEMA, PLUGIN_HOST_APPLY_REQUEST_SCHEMA,
72    PLUGIN_HOST_APPLY_RESULT_SCHEMA, PLUGIN_HOST_CANCEL_REQUEST_SCHEMA,
73    PLUGIN_HOST_CANCEL_RESULT_SCHEMA, PLUGIN_HOST_CAPABILITIES_SCHEMA_V6,
74    PLUGIN_HOST_ENABLEMENT_PLAN_REQUEST_SCHEMA, PLUGIN_HOST_ENABLEMENT_PLAN_RESULT_SCHEMA,
75    PLUGIN_HOST_OBSERVATION_REQUEST_SCHEMA, PLUGIN_HOST_OBSERVATION_RESULT_SCHEMA,
76    PLUGIN_HOST_OPERATION_OBSERVATION_REQUEST_SCHEMA,
77    PLUGIN_HOST_OPERATION_OBSERVATION_RESULT_SCHEMA, PLUGIN_HOST_OPERATION_WATCH_REQUEST_SCHEMA,
78    PLUGIN_HOST_PLAN_REQUEST_SCHEMA, PLUGIN_HOST_PLAN_RESULT_SCHEMA, PLUGIN_HOST_PROTOCOL_LEVEL_V6,
79    PLUGIN_MANAGED_SCOPE_SCHEMA_V2, PLUGIN_MANAGER_TOOLSET_SCHEMA_V4,
80    PLUGIN_OPERATION_CONFIRMATION_SCHEMA, PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3,
81    PLUGIN_OPERATION_PLAN_SCHEMA_V4, PLUGIN_PACKAGE_LOCK_SCHEMA, PLUGIN_PERMISSION_SCHEMA,
82    PLUGIN_PLANNING_BUNDLE_SCHEMA, PLUGIN_WORKSPACE_GRANT_CHANGE_SET_SCHEMA,
83    PLUGIN_WORKSPACE_GRANT_PROPOSAL_SCHEMA, PLUGIN_WORKSPACE_GRANT_SCHEMA,
84    PLUGIN_WORKSPACE_GRANT_SNAPSHOT_SCHEMA,
85};
86pub use release::{
87    HttpHealthContract, McpReleaseDescriptor, McpServiceContract, McpServiceTransport,
88    ReleaseArtifact, ReleaseCompatibility, ReleaseDependency, ReleaseKind, ReleaseProvenance,
89    ReleaseResolution, SkillBindingContract, SkillBindingTarget, SkillContentContract,
90    SkillReleaseDescriptor, ToolReleaseDescriptor, ToolServiceInterface, ToolServiceNetwork,
91    ToolTaskInterface, ToolWorkloadContract, MAX_RELEASE_DESCRIPTOR_BYTES, MCP_RELEASE_SCHEMA,
92    SKILL_RELEASE_SCHEMA, TOOL_RELEASE_SCHEMA,
93};
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96#[serde(transparent)]
97pub struct UseSessionId(String);
98
99impl UseSessionId {
100    pub fn parse(value: impl Into<String>) -> Result<Self, UseError> {
101        let value = value.into();
102        if value.is_empty()
103            || !value.chars().all(|character| {
104                character.is_ascii_alphanumeric() || matches!(character, '-' | '_')
105            })
106        {
107            return Err(UseError::new(
108                "use.session.invalid_id",
109                "Session IDs may contain only ASCII letters, digits, '-' and '_'.",
110            ));
111        }
112        Ok(Self(value))
113    }
114
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "kebab-case")]
122pub enum RiskClass {
123    Read,
124    Navigate,
125    Mutate,
126    Submit,
127    Download,
128    Execute,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct Artifact {
134    pub path: PathBuf,
135    pub media_type: String,
136    pub size: u64,
137    pub sha256: String,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "kebab-case")]
142pub enum Readiness {
143    Ready,
144    Missing,
145    Broken,
146    Unknown,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct DomainDiagnostic {
152    pub domain: String,
153    pub readiness: Readiness,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub provider: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub version: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub path: Option<PathBuf>,
160    pub message: String,
161    #[serde(default, skip_serializing_if = "Vec::is_empty")]
162    pub suggestions: Vec<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct UseError {
168    pub code: String,
169    pub message: String,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub suggestion: Option<String>,
172    #[serde(default)]
173    pub details: BTreeMap<String, serde_json::Value>,
174}
175
176impl UseError {
177    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
178        Self {
179            code: code.into(),
180            message: message.into(),
181            suggestion: None,
182            details: BTreeMap::new(),
183        }
184    }
185
186    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
187        self.suggestion = Some(suggestion.into());
188        self
189    }
190
191    pub fn with_detail(
192        mut self,
193        name: impl Into<String>,
194        value: impl Into<serde_json::Value>,
195    ) -> Self {
196        self.details.insert(name.into(), value.into());
197        self
198    }
199}
200
201impl fmt::Display for UseError {
202    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203        formatter.write_str(&self.message)
204    }
205}
206
207impl std::error::Error for UseError {}
208
209pub type UseResult<T> = Result<T, UseError>;
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum FirstUseInstallBlock {
213    Offline,
214    Disabled,
215}
216
217impl FirstUseInstallBlock {
218    pub const fn reason(self) -> &'static str {
219        match self {
220            Self::Offline => "offline mode",
221            Self::Disabled => "A3S_NO_AUTO_INSTALL",
222        }
223    }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub struct FirstUseInstallPolicy {
228    offline: bool,
229    disabled: bool,
230}
231
232impl FirstUseInstallPolicy {
233    pub const fn new(offline: bool, disabled: bool) -> Self {
234        Self { offline, disabled }
235    }
236
237    pub fn from_env() -> UseResult<Self> {
238        Self::from_values(
239            std::env::var_os("A3S_OFFLINE"),
240            std::env::var_os("A3S_NO_AUTO_INSTALL"),
241        )
242    }
243
244    pub const fn blocked_by(self) -> Option<FirstUseInstallBlock> {
245        if self.offline {
246            Some(FirstUseInstallBlock::Offline)
247        } else if self.disabled {
248            Some(FirstUseInstallBlock::Disabled)
249        } else {
250            None
251        }
252    }
253
254    pub const fn allows_install(self) -> bool {
255        self.blocked_by().is_none()
256    }
257
258    fn from_values(offline: Option<OsString>, disabled: Option<OsString>) -> UseResult<Self> {
259        Ok(Self {
260            offline: parse_environment_boolean("A3S_OFFLINE", offline)?,
261            disabled: parse_environment_boolean("A3S_NO_AUTO_INSTALL", disabled)?,
262        })
263    }
264}
265
266fn parse_environment_boolean(name: &'static str, value: Option<OsString>) -> UseResult<bool> {
267    let Some(value) = value else {
268        return Ok(false);
269    };
270    if value.is_empty() {
271        return Ok(true);
272    }
273    let value = value.into_string().map_err(|_| {
274        UseError::new(
275            "use.first_use.policy_invalid",
276            format!("{name} must contain a valid UTF-8 boolean value."),
277        )
278        .with_detail("variable", name)
279    })?;
280    match value.trim().to_ascii_lowercase().as_str() {
281        "1" | "true" | "yes" | "on" => Ok(true),
282        "0" | "false" | "no" | "off" => Ok(false),
283        _ => Err(UseError::new(
284            "use.first_use.policy_invalid",
285            format!("{name} must be a boolean value."),
286        )
287        .with_detail("variable", name)),
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn session_ids_are_bounded_and_serializable() {
297        let id = UseSessionId::parse("browser_01").unwrap();
298        assert_eq!(id.as_str(), "browser_01");
299        assert!(UseSessionId::parse("../escape").is_err());
300        assert_eq!(serde_json::to_string(&id).unwrap(), "\"browser_01\"");
301    }
302
303    #[test]
304    fn stable_error_has_machine_code_and_suggestion() {
305        let error = UseError::new("use.runtime.missing", "Runtime is missing.")
306            .with_suggestion("Run a3s install use/browser.");
307        let value = serde_json::to_value(error).unwrap();
308        assert_eq!(value["code"], "use.runtime.missing");
309        assert!(value["suggestion"]
310            .as_str()
311            .unwrap()
312            .contains("a3s install"));
313    }
314
315    #[test]
316    fn first_use_policy_uses_a3s_boolean_conventions() {
317        for value in [None, Some("0"), Some("false"), Some("no"), Some("off")] {
318            let policy = FirstUseInstallPolicy::from_values(value.map(Into::into), None).unwrap();
319            assert!(policy.allows_install());
320        }
321        for value in [Some(""), Some("1"), Some("true"), Some("yes"), Some("on")] {
322            let policy = FirstUseInstallPolicy::from_values(value.map(Into::into), None).unwrap();
323            assert_eq!(policy.blocked_by(), Some(FirstUseInstallBlock::Offline));
324        }
325    }
326
327    #[test]
328    fn offline_policy_takes_precedence_over_no_auto_install() {
329        let policy =
330            FirstUseInstallPolicy::from_values(Some("1".into()), Some("1".into())).unwrap();
331        assert_eq!(policy.blocked_by(), Some(FirstUseInstallBlock::Offline));
332    }
333
334    #[test]
335    fn invalid_first_use_policy_is_typed() {
336        let error = FirstUseInstallPolicy::from_values(Some("sometimes".into()), None).unwrap_err();
337        assert_eq!(error.code, "use.first_use.policy_invalid");
338        assert_eq!(error.details["variable"], "A3S_OFFLINE");
339    }
340}