Skip to main content

pointlock_provider_kit/
manifest.rs

1//! Static provider capability declaration (spine §4.1 `ProviderManifest`).
2//!
3//! The manifest ships inside the provider package, versioned with it, and is
4//! consumed by the compiler without any device online (declaration precedes
5//! execution, 04 §1 rule 3). Shapes mirror the spine §4.1 TS signatures
6//! verbatim on the wire (camelCase, closed objects).
7
8use std::collections::BTreeMap;
9
10use pointlock_ir::{ActionName, CanonicalVerb, Channel, FeatureId, JsonSchemaDocument};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14/// Platform discriminator of a device (spine §4.1 `PlatformKind`).
15#[derive(
16    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
17)]
18#[serde(rename_all = "camelCase")]
19pub enum PlatformKind {
20    /// Android device.
21    Android,
22    /// iOS device.
23    Ios,
24    /// Web browser target.
25    Web,
26    /// HarmonyOS device.
27    HarmonyOs,
28    /// macOS desktop.
29    MacOs,
30    /// Windows desktop.
31    Windows,
32    /// Linux desktop.
33    Linux,
34    /// RDP-attached desktop.
35    Rdp,
36}
37
38/// Protocol-level action protection (DeviceRail `ActionProtection`,
39/// spine A.8). This is the *protocol* two-valued enum; the IR-side
40/// `Protection` literal marker additionally pins bindable actions to
41/// `"standard"` in v0.1 (R6, 02 §11).
42#[derive(
43    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
44)]
45#[serde(rename_all = "camelCase")]
46pub enum ActionProtection {
47    /// Regular action; bindable in v0.1.
48    Standard,
49    /// Protected action; rejected at bind in v0.1 (spine R6).
50    Protected,
51}
52
53/// Role a channel plays for this provider (spine §4.1 `ChannelSupport.role`).
54#[derive(
55    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
56)]
57#[serde(rename_all = "camelCase")]
58pub enum ChannelRole {
59    /// The channel can locate/act only.
60    Act,
61    /// The channel can verify only (the only legal role for `vision`).
62    Verify,
63    /// The channel can both act and verify.
64    Both,
65}
66
67/// The protocol version window the provider supports (spine §4.1
68/// `ProviderManifest.protocol`).
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct ProtocolRange {
72    /// Supported major version.
73    pub major: u64,
74    /// Lowest supported minor version.
75    pub min_minor: u64,
76    /// Highest supported minor version.
77    pub max_minor: u64,
78}
79
80/// A feature the provider offers only under some platform condition
81/// (spine §4.1 `ProviderManifest.features.conditional[]`).
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ConditionalFeature {
85    /// The feature id.
86    pub feature: FeatureId,
87    /// Platforms on which the feature is available; absent = unconditional
88    /// on platform (some other runtime condition applies).
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub requires_platform: Option<Vec<PlatformKind>>,
91}
92
93/// The provider's feature declaration (spine §4.1
94/// `ProviderManifest.features`). The compile-time available feature set is
95/// `guaranteed ∪ lockfile.hello.featuresEnabled` (spine §4.1 compile rule).
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "camelCase", deny_unknown_fields)]
98pub struct FeatureDeclarations {
99    /// Features provided unconditionally.
100    pub guaranteed: Vec<FeatureId>,
101    /// Features provided only under declared conditions.
102    pub conditional: Vec<ConditionalFeature>,
103}
104
105/// Declarative canonical-verb → native-action mapping (spine §4.1
106/// `VerbBinding`; R7: the compiler executes zero provider code).
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109pub struct VerbBinding {
110    /// The canonical verb being bound.
111    pub verb: CanonicalVerb,
112    /// The provider-native action name (e.g. `tap` → `tapElement`).
113    pub action_name: ActionName,
114    /// Feature gating the binding (e.g. the semantic five →
115    /// `device.semanticActions.v1`).
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub requires_feature: Option<FeatureId>,
118    /// Declarative field mapping from verb argument names to native action
119    /// argument names.
120    pub arg_map: BTreeMap<String, String>,
121}
122
123/// A channel the provider supports and in which role (spine §4.1
124/// `ChannelSupport`).
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct ChannelSupport {
128    /// The channel.
129    pub channel: Channel,
130    /// Its role; `vision` may only declare `verify` (principle 7).
131    pub role: ChannelRole,
132    /// Feature gating the channel (e.g. `uiTree` →
133    /// `observation.uiSnapshot.v1`).
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub requires_feature: Option<FeatureId>,
136    /// Platforms on which the channel is available.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub requires_platform: Option<Vec<PlatformKind>>,
139}
140
141/// Static definition of one provider action (spine §4.1
142/// `ActionDefinitionStatic`; protocol-level actions such as the semantic
143/// five can be built in).
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
145#[serde(rename_all = "camelCase", deny_unknown_fields)]
146pub struct ActionDefinitionStatic {
147    /// Provider-native action name.
148    pub name: ActionName,
149    /// JSON Schema (Draft 2020-12) of the action arguments — same source as
150    /// the protocol `ActionDefinition.inputSchema`.
151    pub input_schema: JsonSchemaDocument,
152    /// Pointlock-side output schema supplement (the protocol-side output is
153    /// unconstrained JSON).
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub output_schema: Option<JsonSchemaDocument>,
156    /// Protection level of the action.
157    pub protection: ActionProtection,
158    /// Provider-synthetic: the provider implements this action itself
159    /// rather than forwarding it to a driver action (04 §9.4.3).
160    ///
161    /// `observe` and `screenshot` are the only such actions today: they
162    /// map to the `device.observe` RPC, so no driver declares them and
163    /// they never appear in a lockfile's action set. Bind therefore
164    /// overlays them onto the lockfile's actions — **shadowed by name**,
165    /// so a driver action of the same name wins and this entry is
166    /// disabled for that lockfile.
167    ///
168    /// Only readonly actions may be synthetic: they are exempt from
169    /// reconcile by construction, since a dangling readonly intent is
170    /// always safe to replay (spine §6.7-B).
171    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
172    pub synthetic: bool,
173}
174
175/// The provider-package-shipped, package-versioned capability declaration
176/// (spine §4.1 `ProviderManifest`). Consumed by the compiler; no device
177/// needs to be online.
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
179#[serde(rename_all = "camelCase", deny_unknown_fields)]
180pub struct ProviderManifest {
181    /// Provider name (e.g. `"devicerail"`).
182    pub name: String,
183    /// Provider package version.
184    pub version: String,
185    /// Supported protocol window (e.g. supports 1.5).
186    pub protocol: ProtocolRange,
187    /// Feature declaration.
188    pub features: FeatureDeclarations,
189    /// Declarative (non-code) verb → native action mappings.
190    pub verb_bindings: Vec<VerbBinding>,
191    /// Supported channels.
192    pub channels: Vec<ChannelSupport>,
193    /// Built-in protocol-level action definitions. Without a lockfile the
194    /// compiler falls back to these, restricted to actions covered by
195    /// `guaranteed` features (spine §4.1 compile rule).
196    pub known_actions: Vec<ActionDefinitionStatic>,
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use serde_json::json;
203
204    fn sample_manifest() -> ProviderManifest {
205        ProviderManifest {
206            name: "devicerail".to_owned(),
207            version: "0.1.0".to_owned(),
208            protocol: ProtocolRange {
209                major: 1,
210                min_minor: 5,
211                max_minor: 5,
212            },
213            features: FeatureDeclarations {
214                guaranteed: vec![FeatureId::new("device.semanticActions.v1").unwrap()],
215                conditional: vec![ConditionalFeature {
216                    feature: FeatureId::new("observation.uiSnapshot.v1").unwrap(),
217                    requires_platform: Some(vec![PlatformKind::Android, PlatformKind::HarmonyOs]),
218                }],
219            },
220            verb_bindings: vec![VerbBinding {
221                verb: CanonicalVerb::Tap,
222                action_name: ActionName::new("tapElement").unwrap(),
223                requires_feature: Some(FeatureId::new("device.semanticActions.v1").unwrap()),
224                arg_map: BTreeMap::from([("element".to_owned(), "element".to_owned())]),
225            }],
226            channels: vec![ChannelSupport {
227                channel: Channel::UiTree,
228                role: ChannelRole::Both,
229                requires_feature: Some(FeatureId::new("observation.uiSnapshot.v1").unwrap()),
230                requires_platform: None,
231            }],
232            known_actions: vec![ActionDefinitionStatic {
233                name: ActionName::new("tapElement").unwrap(),
234                input_schema: JsonSchemaDocument::new(json!(true)).unwrap(),
235                output_schema: None,
236                protection: ActionProtection::Standard,
237                synthetic: false,
238            }],
239        }
240    }
241
242    #[test]
243    fn manifest_wire_shape_is_camel_case() {
244        let wire = serde_json::to_value(sample_manifest()).expect("serialize");
245        assert_eq!(wire["protocol"]["minMinor"], 5);
246        assert_eq!(wire["verbBindings"][0]["verb"], "tap");
247        assert_eq!(wire["verbBindings"][0]["actionName"], "tapElement");
248        assert_eq!(wire["verbBindings"][0]["argMap"]["element"], "element");
249        assert_eq!(
250            wire["features"]["conditional"][0]["requiresPlatform"],
251            json!(["android", "harmonyOs"])
252        );
253        assert_eq!(wire["channels"][0]["role"], "both");
254        assert_eq!(wire["knownActions"][0]["protection"], "standard");
255        let back: ProviderManifest = serde_json::from_value(wire).expect("deserialize");
256        assert_eq!(back, sample_manifest());
257    }
258
259    #[test]
260    fn manifest_rejects_unknown_fields() {
261        let mut wire = serde_json::to_value(sample_manifest()).expect("serialize");
262        wire["surprise"] = json!(1);
263        assert!(serde_json::from_value::<ProviderManifest>(wire).is_err());
264    }
265
266    #[test]
267    fn platform_kind_wire_literals() {
268        for (kind, literal) in [
269            (PlatformKind::Android, "android"),
270            (PlatformKind::Ios, "ios"),
271            (PlatformKind::Web, "web"),
272            (PlatformKind::HarmonyOs, "harmonyOs"),
273            (PlatformKind::MacOs, "macOs"),
274            (PlatformKind::Windows, "windows"),
275            (PlatformKind::Linux, "linux"),
276            (PlatformKind::Rdp, "rdp"),
277        ] {
278            assert_eq!(serde_json::to_value(kind).unwrap(), json!(literal));
279        }
280    }
281}