1use crate::browser::policy::{
9 BrowserPolicy, POLICY_SCHEMA_VERSION, PolicyCapability, PolicyDecision, PolicyPreset,
10};
11use crate::browser::session::{
12 INTENT_RESOLUTION_SCHEMA_VERSION, KNOWLEDGE_SCHEMA_VERSION,
13 SEMANTIC_OBSERVATION_SCHEMA_VERSION, WORKFLOW_AUTHORING_SCHEMA_VERSION,
14 WORKFLOW_SCHEMA_VERSION,
15};
16use crate::reliability::{
17 RELIABILITY_FIXTURE_SCHEMA_VERSION, RELIABILITY_REPLAY_SCHEMA_VERSION,
18 RELIABILITY_SCENARIO_SCHEMA_VERSION,
19};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::collections::BTreeMap;
23use std::fmt;
24
25pub use crate::protocol::GLASS_PROTOCOL_VERSION;
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct GlassCapabilityManifest {
32 pub protocol_version: u32,
33 pub glass_version: String,
34 pub schemas: BTreeMap<String, Vec<u32>>,
35 pub capabilities: BTreeMap<String, bool>,
36 pub constraints: GlassCapabilityConstraints,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct GlassCapabilityConstraints {
43 pub platform: String,
44 pub browser_family: String,
45 pub policy: String,
46 pub max_sessions: u32,
47}
48
49#[derive(Debug, Deserialize)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51struct GlassCapabilityRequest {
52 #[serde(default = "default_protocol_version")]
53 protocol_version: u32,
54 #[serde(default)]
55 schemas: BTreeMap<String, Vec<u32>>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct CapabilityNegotiationError {
61 pub field: String,
62 pub detail: String,
63}
64
65impl fmt::Display for CapabilityNegotiationError {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(formatter, "{}: {}", self.field, self.detail)
68 }
69}
70
71impl std::error::Error for CapabilityNegotiationError {}
72
73impl GlassCapabilityManifest {
74 pub fn for_policy(policy: &BrowserPolicy) -> Self {
76 Self::for_policy_in_mode(policy, false)
77 }
78
79 pub fn for_policy_in_mode(policy: &BrowserPolicy, local_daemon: bool) -> Self {
81 let raw_cdp = matches!(
82 policy.decide(PolicyCapability::RawCdp),
83 PolicyDecision::Allow
84 );
85 let persistent_profile = matches!(
86 policy.decide(PolicyCapability::PersistentProfile),
87 PolicyDecision::Allow
88 );
89 let mut capabilities = BTreeMap::from([
90 ("action", true),
91 ("semanticRegions", true),
92 ("observationDiffs", true),
93 ("intentResolution", true),
94 ("workflowRuntime", true),
95 ("workflowResume", true),
96 ("persistentKnowledge", persistent_profile),
97 ("workflowAuthoring", true),
98 ("reliabilityCertification", true),
99 ("rawCdp", raw_cdp),
100 ("localDaemon", local_daemon),
101 ("extensions", false),
102 ])
103 .into_iter()
104 .map(|(name, enabled)| (name.to_string(), enabled))
105 .collect::<BTreeMap<_, _>>();
106 capabilities.insert("mcpStdio".into(), true);
107 Self {
108 protocol_version: GLASS_PROTOCOL_VERSION,
109 glass_version: env!("CARGO_PKG_VERSION").into(),
110 schemas: supported_schemas(),
111 capabilities,
112 constraints: GlassCapabilityConstraints {
113 platform: platform_label().into(),
114 browser_family: "chromium".into(),
115 policy: policy_label(policy.preset()).into(),
116 max_sessions: 4,
117 },
118 }
119 }
120
121 pub fn negotiate(&self, request: Option<&Value>) -> Result<Self, CapabilityNegotiationError> {
123 let Some(request) = request else {
124 return Ok(self.clone());
125 };
126 let request: GlassCapabilityRequest =
127 serde_json::from_value(request.clone()).map_err(|error| {
128 CapabilityNegotiationError {
129 field: "glass".into(),
130 detail: format!("invalid capability request: {error}"),
131 }
132 })?;
133 if request.protocol_version != self.protocol_version {
134 return Err(CapabilityNegotiationError {
135 field: "glass.protocolVersion".into(),
136 detail: format!(
137 "unsupported protocol {}; expected {}",
138 request.protocol_version, self.protocol_version
139 ),
140 });
141 }
142 for (name, requested_versions) in request.schemas {
143 let Some(supported_versions) = self.schemas.get(&name) else {
144 return Err(CapabilityNegotiationError {
145 field: format!("glass.schemas.{name}"),
146 detail: "unknown schema".into(),
147 });
148 };
149 if requested_versions.is_empty()
150 || !requested_versions
151 .iter()
152 .any(|version| supported_versions.contains(version))
153 {
154 return Err(CapabilityNegotiationError {
155 field: format!("glass.schemas.{name}"),
156 detail: format!(
157 "requested versions do not intersect supported versions {supported_versions:?}"
158 ),
159 });
160 }
161 }
162 Ok(self.clone())
163 }
164}
165
166fn supported_schemas() -> BTreeMap<String, Vec<u32>> {
167 BTreeMap::from([
168 ("protocol".into(), vec![GLASS_PROTOCOL_VERSION]),
169 ("action".into(), vec![1]),
170 (
171 "observation".into(),
172 vec![SEMANTIC_OBSERVATION_SCHEMA_VERSION],
173 ),
174 ("workflow".into(), vec![WORKFLOW_SCHEMA_VERSION]),
175 ("checkpoint".into(), vec![1]),
176 ("policy".into(), vec![POLICY_SCHEMA_VERSION]),
177 ("workflowCheckpoint".into(), vec![1]),
178 ("trace".into(), vec![1]),
179 ("intent".into(), vec![INTENT_RESOLUTION_SCHEMA_VERSION]),
180 ("knowledge".into(), vec![KNOWLEDGE_SCHEMA_VERSION]),
181 ("authoring".into(), vec![WORKFLOW_AUTHORING_SCHEMA_VERSION]),
182 (
183 "reliabilityScenario".into(),
184 vec![RELIABILITY_SCENARIO_SCHEMA_VERSION],
185 ),
186 (
187 "reliabilityFixture".into(),
188 vec![RELIABILITY_FIXTURE_SCHEMA_VERSION],
189 ),
190 (
191 "reliabilityReplay".into(),
192 vec![RELIABILITY_REPLAY_SCHEMA_VERSION],
193 ),
194 ])
195}
196
197fn default_protocol_version() -> u32 {
198 GLASS_PROTOCOL_VERSION
199}
200
201fn policy_label(policy: PolicyPreset) -> &'static str {
202 match policy {
203 PolicyPreset::Development => "development",
204 PolicyPreset::Ci => "ci",
205 PolicyPreset::Polite => "polite",
206 PolicyPreset::Hardened => "hardened",
207 PolicyPreset::UntrustedMcp => "untrusted-mcp",
208 }
209}
210
211fn platform_label() -> &'static str {
212 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
213 {
214 "linux-x86_64"
215 }
216 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
217 {
218 "linux-arm64"
219 }
220 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
221 {
222 "macos-x86_64"
223 }
224 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
225 {
226 "macos-aarch64"
227 }
228 #[cfg(not(any(
229 all(target_os = "linux", target_arch = "x86_64"),
230 all(target_os = "linux", target_arch = "aarch64"),
231 all(target_os = "macos", target_arch = "x86_64"),
232 all(target_os = "macos", target_arch = "aarch64")
233 )))]
234 {
235 "unsupported"
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn development_manifest() -> GlassCapabilityManifest {
244 let policy = BrowserPolicy::development(std::env::current_dir().unwrap()).unwrap();
245 GlassCapabilityManifest::for_policy(&policy)
246 }
247
248 #[test]
249 fn manifest_is_stable_and_lists_current_contracts() {
250 let manifest = development_manifest();
251
252 assert_eq!(manifest.protocol_version, 1);
253 assert_eq!(manifest.schemas["workflow"], vec![1]);
254 assert_eq!(manifest.schemas["reliabilityReplay"], vec![1]);
255 assert_eq!(manifest.constraints.max_sessions, 4);
256 assert!(manifest.capabilities["workflowResume"]);
257 assert!(!manifest.capabilities["localDaemon"]);
258 }
259
260 #[test]
261 fn negotiation_accepts_compatible_schema_requests() {
262 let manifest = development_manifest();
263 let request = serde_json::json!({
264 "protocolVersion": 1,
265 "schemas": {"action": [1], "workflow": [1]}
266 });
267
268 assert_eq!(manifest.negotiate(Some(&request)).unwrap(), manifest);
269 }
270
271 #[test]
272 fn negotiation_rejects_unknown_and_incompatible_requests() {
273 let manifest = development_manifest();
274 let unknown = serde_json::json!({
275 "protocolVersion": 1,
276 "schemas": {"future": [1]}
277 });
278 let incompatible = serde_json::json!({
279 "protocolVersion": 1,
280 "schemas": {"workflow": [99]}
281 });
282
283 assert!(manifest.negotiate(Some(&unknown)).is_err());
284 assert!(manifest.negotiate(Some(&incompatible)).is_err());
285 }
286}