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}