···88 use std::rc::{Rc, Weak};
99 use std::sync::Arc;
1010 use std::thread::JoinHandle;
1111-@@ -107,12 +108,12 @@
1111+@@ -99,6 +100,7 @@
1212+ BackgroundHangMonitorControlMsg, BackgroundHangMonitorRegister, HangMonitorAlert,
1313+ };
1414+ use content_security_policy::sandboxing_directive::SandboxingFlagSet;
1515++use content_security_policy::url::Url;
1616+ use crossbeam_channel::{Receiver, Select, Sender, unbounded};
1717+ use devtools_traits::{
1818+ ChromeToDevtoolsControlMsg, DevtoolsControlMsg, DevtoolsPageInfo, NavigationState,
1919+@@ -107,12 +109,12 @@
1220 use embedder_traits::resources::{self, Resource};
1321 use embedder_traits::user_contents::{UserContentManagerId, UserContents};
1422 use embedder_traits::{
···2735 };
2836 use euclid::Size2D;
2937 use euclid::default::Size2D as UntypedSize2D;
3030-@@ -159,12 +160,14 @@
3838+@@ -159,12 +161,14 @@
3139 use servo_canvas_traits::webgl::WebGLThreads;
3240 use servo_config::{opts, pref};
3341 use servo_constellation_traits::{
···4856 };
4957 use servo_url::{Host, ImmutableOrigin, ServoUrl};
5058 use storage_traits::StorageThreads;
5151-@@ -178,6 +181,7 @@
5959+@@ -178,6 +182,7 @@
5260 use webgpu_traits::{WebGPU, WebGPURequest};
53615462 use super::embedder::ConstellationToEmbedderMsg;
···5664 use crate::broadcastchannel::BroadcastChannels;
5765 use crate::browsingcontext::{
5866 AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator,
5959-@@ -185,6 +189,7 @@
6767+@@ -185,10 +190,12 @@
6068 };
6169 use crate::constellation_webview::ConstellationWebView;
6270 use crate::event_loop::EventLoop;
···6472 use crate::pipeline::Pipeline;
6573 use crate::process_manager::ProcessManager;
6674 use crate::serviceworker::ServiceWorkerUnprivilegedContent;
6767-@@ -213,6 +218,9 @@
7575+ use crate::session_history::{NeedsToReload, SessionHistoryChange, SessionHistoryDiff};
7676++use crate::tasks;
7777+7878+ type PendingApprovalNavigations = FxHashMap<PipelineId, (LoadData, NavigationHistoryBehavior)>;
7979+8080+@@ -213,6 +220,12 @@
6881 /// While a completion failed, another global requested to complete the transfer.
6982 /// We are still buffering messages, and awaiting the return of the buffer from the global who failed.
7083 CompletionRequested(MessagePortRouterId, VecDeque<PortMessageTask>),
7184+ /// The port is managed by a remote P2P peer.
7285+ /// Messages routed to this port are serialized and sent over the P2P link.
7386+ Remote(String),
8787++ /// The port is a virtual caller-side port for a web task delegation.
8888++ /// Messages routed to this port resolve the caller's promise via the stored callback.
8989++ Task(String),
7490 }
75917692 #[derive(Debug)]
7777-@@ -514,6 +522,25 @@
9393+@@ -514,6 +527,31 @@
7894 /// to the `UserContents` need to be forwared to all the `ScriptThread`s that host
7995 /// the relevant `WebView`.
8096 pub(crate) user_contents_for_manager_id: FxHashMap<UserContentManagerId, UserContents>,
···97113+
98114+ /// The main process side of the ATProdo DOM API.
99115+ at_proto: AtProtoManager,
116116++
117117++ /// Registry of web task providers for the delegation system.
118118++ task_registry: tasks::TaskRegistry,
119119++
120120++ /// Pending web task requests awaiting provider selection or result.
121121++ pending_task_requests: HashMap<String, tasks::PendingTaskRequest>,
100122 }
101123102124 /// State needed to construct a constellation.
103103-@@ -574,6 +601,9 @@
125125+@@ -574,6 +612,9 @@
104126105127 /// The async runtime.
106128 pub async_runtime: Box<dyn AsyncRuntime>,
···110132 }
111133112134 /// When we are exiting a pipeline, we can either force exiting or not. A normal exit
113113-@@ -683,7 +713,7 @@
135135+@@ -683,7 +724,7 @@
114136 script_to_devtools_callback: Default::default(),
115137 #[cfg(feature = "bluetooth")]
116138 bluetooth_ipc_sender: state.bluetooth_thread,
···119141 private_resource_threads: state.private_resource_threads,
120142 public_storage_threads: state.public_storage_threads,
121143 private_storage_threads: state.private_storage_threads,
122122-@@ -735,6 +765,13 @@
144144+@@ -735,6 +776,15 @@
123145 pending_viewport_changes: Default::default(),
124146 screenshot_readiness_requests: Vec::new(),
125147 user_contents_for_manager_id: Default::default(),
···130152+ at_proto: AtProtoManager::new(
131153+ state.public_resource_threads.core_thread,
132154+ ),
155155++ task_registry: tasks::TaskRegistry::new(state.config_dir.as_deref()),
156156++ pending_task_requests: HashMap::new(),
133157 };
134158135159 constellation.run();
136136-@@ -760,6 +797,18 @@
160160+@@ -760,6 +810,18 @@
137161 fn clean_up_finished_script_event_loops(&mut self) {
138162 self.event_loop_join_handles
139163 .retain(|join_handle| !join_handle.is_finished());
···152176 self.event_loops
153177 .retain(|event_loop| event_loop.upgrade().is_some());
154178 }
155155-@@ -1052,6 +1101,11 @@
179179+@@ -1052,6 +1114,11 @@
156180 .get(&webview_id)
157181 .and_then(|webview| webview.user_content_manager_id);
158182···164188 let new_pipeline_info = NewPipelineInfo {
165189 parent_info: parent_pipeline_id,
166190 new_pipeline_id,
167167-@@ -1062,6 +1116,13 @@
191191+@@ -1062,6 +1129,13 @@
168192 viewport_details: initial_viewport_details,
169193 user_content_manager_id,
170194 theme,
···178202 };
179203 let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) {
180204 Ok(pipeline) => pipeline,
181181-@@ -1228,6 +1289,7 @@
205205+@@ -1228,6 +1302,7 @@
182206 BackgroundHangMonitor(HangMonitorAlert),
183207 Embedder(EmbedderToConstellationMessage),
184208 FromSWManager(SWManagerMsg),
···186210 RemoveProcess(usize),
187211 }
188212 // Get one incoming request.
189189-@@ -1248,6 +1310,15 @@
213213+@@ -1248,6 +1323,15 @@
190214 sel.recv(&self.embedder_to_constellation_receiver);
191215 sel.recv(&self.swmanager_receiver);
192216···202226 self.process_manager.register(&mut sel);
203227204228 let request = {
205205-@@ -1276,9 +1347,13 @@
229229+@@ -1276,9 +1360,13 @@
206230 .recv(&self.swmanager_receiver)
207231 .expect("Unexpected SW channel panic in constellation")
208232 .map(Request::FromSWManager),
···217241 let _ = oper.recv(self.process_manager.receiver_at(process_index));
218242 Ok(Request::RemoveProcess(process_index))
219243 },
220220-@@ -1304,6 +1379,9 @@
244244+@@ -1304,6 +1392,9 @@
221245 Request::FromSWManager(message) => {
222246 self.handle_request_from_swmanager(message);
223247 },
···227251 Request::RemoveProcess(index) => self.process_manager.remove(index),
228252 }
229253 }
230230-@@ -1532,11 +1610,7 @@
254254+@@ -1532,11 +1623,7 @@
231255 }
232256 },
233257 EmbedderToConstellationMessage::PreferencesUpdated(updates) => {
···240264 let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated(
241265 updates
242266 .iter()
243243-@@ -1563,6 +1637,18 @@
267267+@@ -1563,6 +1650,18 @@
244268 EmbedderToConstellationMessage::SetAccessibilityActive(webview_id, active) => {
245269 self.set_accessibility_active(webview_id, active);
246270 },
···259283 }
260284 }
261285262262-@@ -1760,7 +1846,13 @@
286286+@@ -1760,7 +1859,13 @@
263287 return warn!("Attempt to add channel name from an unexpected origin.");
264288 }
265289 self.broadcast_channels
···274298 },
275299 ScriptToConstellationMessage::RemoveBroadcastChannelNameInRouter(
276300 router_id,
277277-@@ -1774,7 +1866,13 @@
301301+@@ -1774,7 +1879,13 @@
278302 return warn!("Attempt to remove channel name from an unexpected origin.");
279303 }
280304 self.broadcast_channels
···289313 },
290314 ScriptToConstellationMessage::RemoveBroadcastChannelRouter(router_id, origin) => {
291315 if self
292292-@@ -1786,6 +1884,12 @@
316316+@@ -1786,6 +1897,12 @@
293317 self.broadcast_channels
294318 .remove_broadcast_channel_router(router_id);
295319 },
···302326 ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => {
303327 if self
304328 .check_origin_against_pipeline(&source_pipeline_id, &message.origin)
305305-@@ -1795,8 +1899,15 @@
329329+@@ -1795,8 +1912,15 @@
306330 "Attempt to schedule broadcast from an origin not matching the origin of the msg."
307331 );
308332 }
···319343 },
320344 ScriptToConstellationMessage::PipelineExited => {
321345 self.handle_pipeline_exited(source_pipeline_id);
322322-@@ -1816,6 +1927,12 @@
346346+@@ -1816,6 +1940,12 @@
323347 ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => {
324348 self.handle_script_new_auxiliary(load_info);
325349 },
···332356 ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => {
333357 self.handle_change_running_animations_state(source_pipeline_id, animation_state)
334358 },
335335-@@ -1989,6 +2106,29 @@
359359+@@ -1862,7 +1992,7 @@
360360+ ScriptToConstellationMessage::SetFinalUrl(final_url) => {
361361+ // The script may have finished loading after we already started shutting down.
362362+ if let Some(ref mut pipeline) = self.pipelines.get_mut(&source_pipeline_id) {
363363+- pipeline.url = final_url;
364364++ pipeline.url = final_url.clone();
365365+ } else {
366366+ warn!("constellation got set final url message for dead pipeline");
367367+ }
368368+@@ -1989,6 +2119,29 @@
336369 new_value,
337370 );
338371 },
···362395 ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => {
363396 // Unlikely at this point, but we may receive events coming from
364397 // different media sessions, so we set the active media session based
365365-@@ -2008,7 +2148,12 @@
398398+@@ -2008,7 +2161,12 @@
366399 }
367400 self.active_media_session = Some(pipeline_id);
368401 self.constellation_to_embedder_proxy.send(
···376409 );
377410 },
378411 #[cfg(feature = "webgpu")]
379379-@@ -2063,9 +2208,412 @@
412412+@@ -2063,7 +2221,769 @@
380413 let _ = event_loop.send(ScriptThreadMessage::TriggerGarbageCollection);
381414 }
382415 },
···611644+ ScriptToConstellationMessage::AtProto(request, response) => {
612645+ self.at_proto.process_request(request, response);
613646+ },
614614- }
615615- }
616616-647647++ ScriptToConstellationMessage::RequestTask(
648648++ task_name,
649649++ caller_data_json,
650650++ _display,
651651++ data,
652652++ provider_port_id,
653653++ callback,
654654++ ) => {
655655++ debug!("RequestTask: {task_name}");
656656++
657657++ // Parse caller data for filter matching.
658658++ let caller_data: HashMap<String, serde_json::Value> =
659659++ serde_json::from_str(&caller_data_json).unwrap_or_default();
660660++
661661++ // Look up matching providers in the registry.
662662++ let provider_infos = self
663663++ .task_registry
664664++ .get_provider_infos(&task_name, &caller_data);
665665++ let providers_json =
666666++ serde_json::to_string(&provider_infos).unwrap_or_else(|_| "[]".into());
667667++ let request_id = format!("task-{webview_id:?}-{source_pipeline_id:?}");
668668++
669669++ // Register provider_port_id as a Task port in the constellation.
670670++ // When the provider posts on its entangled port, the message arrives here
671671++ // and gets routed through the callback to resolve the caller's promise.
672672++ self.message_ports.insert(
673673++ provider_port_id,
674674++ MessagePortInfo {
675675++ state: TransferState::Task(request_id.clone()),
676676++ entangled_with: None,
677677++ },
678678++ );
679679++
680680++ // Store the pending request with data and callback.
681681++ self.pending_task_requests.insert(
682682++ request_id.clone(),
683683++ tasks::PendingTaskRequest {
684684++ task_name: task_name.clone(),
685685++ data,
686686++ callback,
687687++ provider: None,
688688++ provider_port_id,
689689++ provider_url: None,
690690++ dispatched: false,
691691++ provider_webview_id: None,
692692++ remote_caller_peer_id: None,
693693++ remote_providers: HashMap::new(),
694694++ },
695695++ );
696696++
697697++ // Send to all script threads — the system UI will handle it
698698++ // via navigator.embedder.ontaskrequest.
699699++ for event_loop in self.event_loops() {
700700++ let _ = event_loop.send(ScriptThreadMessage::ShowTaskChooser(
701701++ request_id.clone(),
702702++ task_name.clone(),
703703++ providers_json.clone(),
704704++ ));
705705++ }
706706++
707707++ // Broadcast TaskQuery to paired connected devices for remote provider discovery.
708708++ self.pairing.broadcast_message(&P2pMessage::TaskQuery {
709709++ request_id: request_id.clone(),
710710++ task_name: task_name.clone(),
711711++ caller_data_json: caller_data_json.clone(),
712712++ });
713713++ },
714714++ ScriptToConstellationMessage::RegisterTaskProvider(
715715++ task_name,
716716++ title,
717717++ description,
718718++ href,
719719++ filters_json,
720720++ returns_type,
721721++ display_mode,
722722++ icon,
723723++ ) => {
724724++ debug!("RegisterTaskProvider: {task_name} -> {href}");
725725++ let display = match display_mode.as_str() {
726726++ "inline" => tasks::TaskDisplayMode::Inline,
727727++ _ => tasks::TaskDisplayMode::Window,
728728++ };
729729++ let filters = serde_json::from_str(&filters_json).unwrap_or_default();
730730++ if let Ok(url) = ServoUrl::parse(&href) {
731731++ self.task_registry.register(tasks::TaskProvider {
732732++ task_name,
733733++ title,
734734++ description,
735735++ href: url,
736736++ filters,
737737++ returns_type,
738738++ display,
739739++ icon,
740740++ });
741741++ }
742742++ },
743743++ ScriptToConstellationMessage::TaskProviderSelected(request_id, provider_id) => {
744744++ debug!("TaskProviderSelected: {request_id} -> {provider_id:?}");
745745++
746746++ let Some(mut pending) = self.pending_task_requests.remove(&request_id) else {
747747++ warn!("TaskProviderSelected: unknown request {request_id}");
748748++ return;
749749++ };
750750++
751751++ let Some(provider_id) = provider_id else {
752752++ // User cancelled.
753753++ let _ = pending.callback.send(Err("Task cancelled".into()));
754754++ return;
755755++ };
756756++
757757++ // Check if this is a remote provider (ID starts with "remote:{peer_id}:").
758758++ if provider_id.starts_with("remote:") {
759759++ let parts: Vec<&str> = provider_id.splitn(3, ':').collect();
760760++ if parts.len() >= 3 {
761761++ let peer_id = parts[1];
762762++
763763++ // Look up the remote provider's href from stored info.
764764++ let provider_href = pending
765765++ .remote_providers
766766++ .get(&provider_id)
767767++ .map(|p| p.href.clone())
768768++ .unwrap_or_default();
769769++
770770++ // Serialize the caller's data for P2P transfer.
771771++ let caller_data = pending
772772++ .data
773773++ .as_ref()
774774++ .and_then(|d| postcard::to_allocvec(d).ok())
775775++ .unwrap_or_default();
776776++
777777++ self.pairing.send_message(
778778++ peer_id,
779779++ &P2pMessage::TaskExecute {
780780++ request_id: request_id.clone(),
781781++ task_name: pending.task_name.clone(),
782782++ provider_href,
783783++ caller_data,
784784++ },
785785++ );
786786++
787787++ // Keep the pending request for when TaskResult arrives.
788788++ self.pending_task_requests
789789++ .insert(request_id.clone(), pending);
790790++ }
791791++ return;
792792++ }
793793++
794794++ // Resolve the provider ID to a TaskProvider.
795795++ let Some(provider) = self.task_registry.resolve_provider(&provider_id).cloned()
796796++ else {
797797++ let _ = pending
798798++ .callback
799799++ .send(Err(format!("Unknown provider: {provider_id}")));
800800++ return;
801801++ };
802802++
803803++ // Store the pending request back (with provider info) for when
804804++ // the provider webview opens and later completes.
805805++ pending.provider = Some(provider.clone());
806806++
807807++ // Build the provider URL with the request ID as a query param
808808++ // so the provider pipeline can be correlated.
809809++ let mut provider_url = provider.href.clone().into_url();
810810++ provider_url
811811++ .query_pairs_mut()
812812++ .append_pair("taskRequestId", &request_id);
813813++
814814++ pending.provider_url = Some(provider_url.to_string());
815815++ self.pending_task_requests
816816++ .insert(request_id.clone(), pending);
817817++
818818++ // Tell the system UI to open the provider webview.
819819++ for event_loop in self.event_loops() {
820820++ let _ = event_loop.send(ScriptThreadMessage::OpenTaskProvider(
821821++ request_id.clone(),
822822++ provider_url.to_string(),
823823++ provider.title.clone(),
824824++ ));
825825++ }
826826++ },
827827++ ScriptToConstellationMessage::AcceptTask(callback) => {
828828++ // Find a pending task request whose provider URL matches this pipeline's URL.
829829++ let pipeline_url = self
830830++ .pipelines
831831++ .get(&source_pipeline_id)
832832++ .map(|p| p.url.as_str().to_string());
833833++
834834++ let matching_request = pipeline_url.and_then(|url| {
835835++ self.pending_task_requests
836836++ .iter()
837837++ .find(|(_, p)| p.provider_url.as_deref() == Some(&url) && !p.dispatched)
838838++ .map(|(id, _)| id.clone())
839839++ });
840840++
841841++ if let Some(request_id) = matching_request {
842842++ if let Some(pending) = self.pending_task_requests.get_mut(&request_id) {
843843++ pending.dispatched = true;
844844++ pending.provider_webview_id = Some(webview_id);
845845++ let port_id_bytes =
846846++ postcard::to_allocvec(&pending.provider_port_id).unwrap_or_default();
847847++ let task_name = pending.task_name.clone();
848848++ let data = pending.data.take();
849849++
850850++ let _ = callback.send(Some((task_name, data, port_id_bytes)));
851851++ } else {
852852++ let _ = callback.send(None);
853853++ }
854854++ } else {
855855++ // No matching task request — this page is not a task provider.
856856++ let _ = callback.send(None);
857857++ }
858858++ },
859859++ }
860860++ }
861861++
617862+ fn handle_pairing_event(&mut self, event: PairingEvent) {
618863+ if let PairingEvent::MessageReceived { ref from, ref data } = event {
619864+ debug!("P2P message received from {from}, {} bytes", data.len());
···749994+ self.message_ports.remove(&port_id);
750995+ }
751996+ },
997997++ P2pMessage::TaskQuery {
998998++ ref request_id,
999999++ ref task_name,
10001000++ ref caller_data_json,
10011001++ } => {
10021002++ // Remote device is asking if we have matching providers.
10031003++ let caller_data: HashMap<String, serde_json::Value> =
10041004++ serde_json::from_str(caller_data_json).unwrap_or_default();
10051005++ let provider_infos = self
10061006++ .task_registry
10071007++ .get_provider_infos(task_name, &caller_data);
10081008++ let providers_json =
10091009++ serde_json::to_string(&provider_infos).unwrap_or_else(|_| "[]".into());
10101010++ let device_name = self.pairing.get_local_name_sync();
10111011++ self.pairing.send_message(
10121012++ &from,
10131013++ &P2pMessage::TaskQueryResponse {
10141014++ request_id: request_id.clone(),
10151015++ providers_json,
10161016++ device_name,
10171017++ },
10181018++ );
10191019++ },
10201020++ P2pMessage::TaskQueryResponse {
10211021++ ref request_id,
10221022++ ref providers_json,
10231023++ ref device_name,
10241024++ } => {
10251025++ // Remote device responded with matching providers.
10261026++ // Parse and send update to the system UI.
10271027++ if let Ok(mut remote_providers) =
10281028++ serde_json::from_str::<Vec<tasks::TaskProviderInfo>>(providers_json)
10291029++ {
10301030++ // Tag each provider with the remote device info.
10311031++ for p in &mut remote_providers {
10321032++ p.device_name = Some(device_name.clone());
10331033++ p.remote_peer_id = Some(from.clone());
10341034++ // Prefix the ID to avoid collisions with local providers.
10351035++ p.id = format!("remote:{}:{}", from, p.id);
10361036++ }
10371037++ if !remote_providers.is_empty() {
10381038++ // Store remote providers for later lookup when user selects one.
10391039++ if let Some(pending) =
10401040++ self.pending_task_requests.get_mut(request_id)
10411041++ {
10421042++ for p in &remote_providers {
10431043++ pending.remote_providers.insert(p.id.clone(), p.clone());
10441044++ }
10451045++ }
10461046++
10471047++ let update_json = serde_json::to_string(&remote_providers)
10481048++ .unwrap_or_else(|_| "[]".into());
10491049++ // Send update to system UI via ScriptThreadMessage.
10501050++ for event_loop in self.event_loops() {
10511051++ let _ =
10521052++ event_loop.send(ScriptThreadMessage::TaskProvidersUpdate(
10531053++ request_id.clone(),
10541054++ update_json.clone(),
10551055++ ));
10561056++ }
10571057++ }
10581058++ }
10591059++ },
10601060++ P2pMessage::TaskExecute {
10611061++ ref request_id,
10621062++ ref task_name,
10631063++ ref provider_href,
10641064++ ref caller_data,
10651065++ } => {
10661066++ // Remote device wants us to execute a task.
10671067++ debug!("TaskExecute from {from}: {task_name} -> {provider_href}");
10681068++ let data: Option<StructuredSerializedData> =
10691069++ postcard::from_bytes(caller_data).ok();
10701070++ let provider_port_id = MessagePortId::new();
10711071++
10721072++ // Register the provider port as a remote-task port.
10731073++ // When the provider posts a result, we'll send it back to the caller.
10741074++ self.message_ports.insert(
10751075++ provider_port_id,
10761076++ MessagePortInfo {
10771077++ state: TransferState::Task(request_id.clone()),
10781078++ entangled_with: None,
10791079++ },
10801080++ );
10811081++
10821082++ // Create a no-op callback — the actual result will be sent
10831083++ // back via P2P in the TransferState::Task handler.
10841084++ let noop_callback = GenericCallback::new(
10851085++ move |_: Result<Result<StructuredSerializedData, String>, _>| {},
10861086++ )
10871087++ .expect("Could not create callback");
10881088++
10891089++ // Store the pending request with the remote caller's peer ID.
10901090++ let mut provider_url = Url::parse(provider_href)
10911091++ .unwrap_or_else(|_| Url::parse("about:blank").unwrap());
10921092++ provider_url
10931093++ .query_pairs_mut()
10941094++ .append_pair("taskRequestId", request_id);
10951095++
10961096++ self.pending_task_requests.insert(
10971097++ request_id.clone(),
10981098++ tasks::PendingTaskRequest {
10991099++ task_name: task_name.clone(),
11001100++ data,
11011101++ callback: noop_callback,
11021102++ provider: None,
11031103++ provider_port_id,
11041104++ provider_url: Some(provider_url.to_string()),
11051105++ dispatched: false,
11061106++ provider_webview_id: None,
11071107++ remote_caller_peer_id: Some(from.clone()),
11081108++ remote_providers: HashMap::new(),
11091109++ },
11101110++ );
11111111++
11121112++ // Tell the system UI to open the provider webview.
11131113++ for event_loop in self.event_loops() {
11141114++ let _ = event_loop.send(ScriptThreadMessage::OpenTaskProvider(
11151115++ request_id.clone(),
11161116++ provider_url.to_string(),
11171117++ task_name.clone(),
11181118++ ));
11191119++ }
11201120++ },
11211121++ P2pMessage::TaskResult {
11221122++ ref request_id,
11231123++ success,
11241124++ ref result_data,
11251125++ } => {
11261126++ // Remote device returned a task result.
11271127++ debug!("TaskResult from {from}: {request_id} success={success}");
11281128++ if let Some(pending) = self.pending_task_requests.remove(request_id) {
11291129++ if success {
11301130++ if let Ok(data) =
11311131++ postcard::from_bytes::<StructuredSerializedData>(result_data)
11321132++ {
11331133++ let _ = pending.callback.send(Ok(data));
11341134++ } else {
11351135++ let _ = pending.callback.send(Err(
11361136++ "Failed to deserialize remote task result".into(),
11371137++ ));
11381138++ }
11391139++ } else {
11401140++ let _ = pending.callback.send(Err("Remote task cancelled".into()));
11411141++ }
11421142++ }
11431143++ },
7521144+ _ => {},
7531145+ }
7541146+ }
···7581150+ // Handle peer disconnect: clean up remote channel state.
7591151+ if let PairingEvent::PeerExpired { ref id } = event {
7601152+ self.pairing.clear_remote_peer(id);
761761-+ }
11531153+ }
7621154+
7631155+ // When a peer connects or reconnects, sync our open broadcast channels to it.
7641156+ if let PairingEvent::PeerDiscovered { ref id, .. } |
···7841176+ let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone()));
7851177+ }
7861178+ }
787787-+ }
788788-+
11791179+ }
11801180+7891181 /// Check the origin of a message against that of the pipeline it came from.
790790- /// Note: this is still limited as a security check,
791791- /// see <https://github.com/servo/servo/issues/11722>
792792-@@ -2382,6 +2930,29 @@
11821182+@@ -2382,6 +3302,55 @@
7931183 TransferState::TransferInProgress(queue) => queue.push_back(task),
7941184 TransferState::CompletionFailed(queue) => queue.push_back(task),
7951185 TransferState::CompletionRequested(_, queue) => queue.push_back(task),
···8161206+ },
8171207+ }
8181208+ },
12091209++ TransferState::Task(request_id) => {
12101210++ // The provider posted a result on its port.
12111211++ let request_id = request_id.clone();
12121212++ if let Some(pending) = self.pending_task_requests.remove(&request_id) {
12131213++ if let Some(ref remote_peer_id) = pending.remote_caller_peer_id {
12141214++ // Remote task: send result back via P2P.
12151215++ let result_data = postcard::to_allocvec(&task.data).unwrap_or_default();
12161216++ self.pairing.send_message(
12171217++ remote_peer_id,
12181218++ &P2pMessage::TaskResult {
12191219++ request_id: request_id.clone(),
12201220++ success: true,
12211221++ result_data,
12221222++ },
12231223++ );
12241224++ } else {
12251225++ // Local task: resolve the caller's promise directly.
12261226++ let _ = pending.callback.send(Ok(task.data));
12271227++ }
12281228++
12291229++ // Close the provider webview.
12301230++ if let Some(provider_webview_id) = pending.provider_webview_id {
12311231++ self.handle_close_top_level_browsing_context(provider_webview_id);
12321232++ }
12331233++ }
12341234++ },
8191235 }
8201236 }
8211237822822-@@ -3273,6 +3844,13 @@
12381238+@@ -3273,6 +4242,40 @@
8231239 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable>
8241240 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) {
8251241 debug!("{webview_id}: Closing");
8261242+
12431243++ // If this is a task provider webview, reject the caller's promise.
12441244++ let task_request_id = self
12451245++ .pending_task_requests
12461246++ .iter()
12471247++ .find(|(_, p)| p.provider_webview_id == Some(webview_id))
12481248++ .map(|(id, _)| id.clone());
12491249++ if let Some(request_id) = task_request_id {
12501250++ if let Some(pending) = self.pending_task_requests.remove(&request_id) {
12511251++ if let Some(ref remote_peer_id) = pending.remote_caller_peer_id {
12521252++ // Remote task: send cancellation back via P2P.
12531253++ self.pairing.send_message(
12541254++ remote_peer_id,
12551255++ &P2pMessage::TaskResult {
12561256++ request_id: request_id.clone(),
12571257++ success: false,
12581258++ result_data: vec![],
12591259++ },
12601260++ );
12611261++ } else {
12621262++ // Local task: reject the caller's promise.
12631263++ let _ = pending
12641264++ .callback
12651265++ .send(Err("Task provider closed without completing".into()));
12661266++ }
12671267++ }
12681268++ }
12691269++
8271270+ // Notify embedded webview parent before closing (if this is an embedded webview)
8281271+ self.handle_embedded_webview_notification(webview_id, EmbeddedWebViewEventType::Closed);
8291272+
···8331276 let browsing_context_id = BrowsingContextId::from(webview_id);
8341277 // Step 5. Remove traversable from the user agent's top-level traversable set.
8351278 let browsing_context =
836836-@@ -3547,8 +4125,27 @@
12791279+@@ -3547,8 +4550,27 @@
8371280 opener_webview_id,
8381281 opener_pipeline_id,
8391282 response_sender,
···8611304 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else {
8621305 warn!("Failed to create channel");
8631306 let _ = response_sender.send(None);
864864-@@ -3649,6 +4246,397 @@
13071307+@@ -3649,6 +4671,397 @@
8651308 });
8661309 }
8671310···12591702 #[servo_tracing::instrument(skip_all)]
12601703 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) {
12611704 let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
12621262-@@ -4776,7 +5764,7 @@
17051705+@@ -4776,7 +6189,7 @@
12631706 }
1264170712651708 #[servo_tracing::instrument(skip_all)]
···12681711 // Send a flat projection of the history to embedder.
12691712 // The final vector is a concatenation of the URLs of the past
12701713 // entries, the current entry and the future entries.
12711271-@@ -4880,9 +5868,22 @@
17141714+@@ -4880,9 +6293,22 @@
12721715 self.constellation_to_embedder_proxy
12731716 .send(ConstellationToEmbedderMsg::HistoryChanged(
12741717 webview_id,
+7-1
patches/components/constellation/lib.rs.patch
···88 mod broadcastchannel;
99 mod browsingcontext;
1010 mod constellation;
1111-@@ -14,6 +15,7 @@
1111+@@ -14,11 +15,13 @@
1212 mod embedder;
1313 mod event_loop;
1414 mod logging;
···1616 mod pipeline;
1717 mod process_manager;
1818 mod sandboxing;
1919+ mod serviceworker;
2020+ mod session_history;
2121++mod tasks;
2222+2323+ pub use crate::constellation::{Constellation, InitialConstellationState};
2424+ pub use crate::embedder::ConstellationToEmbedderMsg;
+47-1
patches/components/constellation/pairing.rs.patch
···11--- original
22+++ modified
33-@@ -0,0 +1,816 @@
33+@@ -0,0 +1,862 @@
44+// SPDX-License-Identifier: AGPL-3.0-or-later
55+
66+//! P2P pairing service integration with the constellation.
···6464+ },
6565+ /// Deny a port offer — the remote side refused the stream.
6666+ PortOfferDenied { stream_id: String },
6767++
6868++ // Web Tasks P2P messages
6969++ /// Query remote device for matching task providers.
7070++ TaskQuery {
7171++ request_id: String,
7272++ task_name: String,
7373++ caller_data_json: String,
7474++ },
7575++ /// Response with matching providers from the remote device.
7676++ TaskQueryResponse {
7777++ request_id: String,
7878++ providers_json: String,
7979++ device_name: String,
8080++ },
8181++ /// Request remote device to execute a task with a specific provider.
8282++ TaskExecute {
8383++ request_id: String,
8484++ task_name: String,
8585++ provider_href: String,
8686++ caller_data: Vec<u8>,
8787++ },
8888++ /// Result from a remotely executed task.
8989++ TaskResult {
9090++ request_id: String,
9191++ success: bool,
9292++ result_data: Vec<u8>,
9393++ },
6794+}
6895+
6996+impl P2pMessage {
···96123+ event_receiver: None,
97124+ remote_channels: Default::default(),
98125+ confirmed_peers: Default::default(),
126126++ }
127127++ }
128128++
129129++ /// Get the local device name synchronously (best effort).
130130++ pub(crate) fn get_local_name_sync(&self) -> String {
131131++ let local_info = self.local_info.clone();
132132++ match local_info.try_lock() {
133133++ Ok(guard) => guard
134134++ .as_ref()
135135++ .map(|info| info.name.clone())
136136++ .unwrap_or_else(|| "Unknown device".to_string()),
137137++ Err(_) => "Unknown device".to_string(),
99138+ }
100139+ }
101140+
···622661+ P2pMessage::PortClose { .. } |
623662+ P2pMessage::PortOfferDenied { .. } => {
624663+ // Return to constellation for port routing.
664664++ Some((from.to_owned(), message))
665665++ },
666666++ P2pMessage::TaskQuery { .. } |
667667++ P2pMessage::TaskQueryResponse { .. } |
668668++ P2pMessage::TaskExecute { .. } |
669669++ P2pMessage::TaskResult { .. } => {
670670++ // Return to constellation for task delegation handling.
625671+ Some((from.to_owned(), message))
626672+ },
627673+ }
+518
patches/components/constellation/tasks.rs.patch
···11+--- original
22++++ modified
33+@@ -0,0 +1,515 @@
44++// SPDX-License-Identifier: AGPL-3.0-or-later
55++
66++//! Task provider registry for the Web Tasks delegation system.
77++//!
88++//! The constellation maintains a registry of task providers. Providers can be
99++//! registered by privileged system pages or discovered from web page meta tags.
1010++
1111++use std::collections::HashMap;
1212++use std::fs;
1313++use std::path::{Path, PathBuf};
1414++
1515++use log::{debug, error};
1616++use serde::{Deserialize, Serialize};
1717++use servo_base::generic_channel::GenericCallback;
1818++use servo_base::id::{MessagePortId, WebViewId};
1919++use servo_constellation_traits::StructuredSerializedData;
2020++use servo_url::ServoUrl;
2121++
2222++/// How a task provider should be displayed when launched.
2323++#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
2424++pub enum TaskDisplayMode {
2525++ /// Shown as a webview overlay on top of the caller's webview.
2626++ Inline,
2727++ /// Opened in a new webview (full navigation).
2828++ Window,
2929++}
3030++
3131++/// A filter value for task provider matching.
3232++/// Follows the MozActivities filter spec.
3333++#[derive(Clone, Debug, Deserialize, Serialize)]
3434++#[serde(untagged)]
3535++pub enum FilterValue {
3636++ /// A basic value: optional field, must equal this if present.
3737++ String(String),
3838++ /// A numeric value.
3939++ Number(f64),
4040++ /// An array of allowed values: optional field, must equal one if present.
4141++ StringArray(Vec<String>),
4242++ /// A filter definition object with detailed matching rules.
4343++ Object(FilterObject),
4444++}
4545++
4646++/// Detailed filter rules for a single field.
4747++#[derive(Clone, Debug, Deserialize, Serialize)]
4848++pub struct FilterObject {
4949++ /// If true, the field must exist in the caller's data.
5050++ #[serde(default)]
5151++ pub required: bool,
5252++ /// Allowed value(s). Can be a single value or array.
5353++ #[serde(default)]
5454++ pub value: Option<FilterAllowedValues>,
5555++ /// Minimum numeric value (inclusive).
5656++ #[serde(default)]
5757++ pub min: Option<f64>,
5858++ /// Maximum numeric value (inclusive).
5959++ #[serde(default)]
6060++ pub max: Option<f64>,
6161++ /// Regex pattern the value must match.
6262++ #[serde(default)]
6363++ pub pattern: Option<String>,
6464++ /// Regex flags (e.g., "i" for case-insensitive).
6565++ #[serde(default, rename = "patternFlags")]
6666++ pub pattern_flags: Option<String>,
6767++}
6868++
6969++/// Allowed values: a single value or an array.
7070++#[derive(Clone, Debug, Deserialize, Serialize)]
7171++#[serde(untagged)]
7272++pub enum FilterAllowedValues {
7373++ Single(String),
7474++ Multiple(Vec<String>),
7575++}
7676++
7777++/// A registered task provider.
7878++#[derive(Clone, Debug, Deserialize, Serialize)]
7979++pub struct TaskProvider {
8080++ /// The task name this provider handles (e.g., "pick-image").
8181++ pub task_name: String,
8282++ /// Human-readable title shown in the chooser.
8383++ pub title: String,
8484++ /// Optional description for the chooser.
8585++ pub description: Option<String>,
8686++ /// URL of the handler page.
8787++ pub href: ServoUrl,
8888++ /// Filters for matching. Keys are field names from the caller's data.
8989++ pub filters: HashMap<String, FilterValue>,
9090++ /// What the provider returns: "blob", "text", "json", or "none".
9191++ pub returns_type: Option<String>,
9292++ /// How the provider should be displayed.
9393++ pub display: TaskDisplayMode,
9494++ /// Optional icon as a data: URL (base64 encoded).
9595++ pub icon: Option<String>,
9696++}
9797++
9898++/// Serializable provider info sent to the embedder for the chooser UI.
9999++#[derive(Clone, Debug, Deserialize, Serialize)]
100100++pub struct TaskProviderInfo {
101101++ pub id: String,
102102++ pub title: String,
103103++ pub description: Option<String>,
104104++ pub display: TaskDisplayMode,
105105++ pub icon: Option<String>,
106106++ pub origin: String,
107107++ pub href: String,
108108++ pub device_name: Option<String>,
109109++ pub remote_peer_id: Option<String>,
110110++}
111111++
112112++/// Registry of all known task providers.
113113++pub struct TaskRegistry {
114114++ /// Map from task name → list of providers.
115115++ providers: HashMap<String, Vec<TaskProvider>>,
116116++ /// Path to persist providers.
117117++ config_path: Option<PathBuf>,
118118++}
119119++
120120++impl Default for TaskRegistry {
121121++ fn default() -> Self {
122122++ Self {
123123++ providers: HashMap::new(),
124124++ config_path: None,
125125++ }
126126++ }
127127++}
128128++
129129++impl TaskRegistry {
130130++ pub fn new(config_dir: Option<&Path>) -> Self {
131131++ let mut registry = Self {
132132++ providers: HashMap::new(),
133133++ config_path: config_dir.map(|d| d.join("task-providers.json")),
134134++ };
135135++ registry.load();
136136++ registry
137137++ }
138138++
139139++ /// Register a new task provider. Uses (task_name, href) as the dedup key.
140140++ /// Re-registering with the same key updates the other fields.
141141++ pub fn register(&mut self, provider: TaskProvider) {
142142++ let providers = self
143143++ .providers
144144++ .entry(provider.task_name.clone())
145145++ .or_default();
146146++ let href_str = provider.href.as_str();
147147++
148148++ // Check for existing provider with same href.
149149++ if let Some(existing) = providers.iter_mut().find(|p| p.href.as_str() == href_str) {
150150++ // Update existing registration.
151151++ existing.title = provider.title;
152152++ existing.description = provider.description;
153153++ existing.filters = provider.filters;
154154++ existing.returns_type = provider.returns_type;
155155++ existing.display = provider.display;
156156++ existing.icon = provider.icon;
157157++ debug!(
158158++ "Updated task provider: {} -> {}",
159159++ existing.task_name, href_str
160160++ );
161161++ } else {
162162++ debug!(
163163++ "Registered task provider: {} -> {}",
164164++ provider.task_name, href_str
165165++ );
166166++ providers.push(provider);
167167++ }
168168++
169169++ self.save();
170170++ }
171171++
172172++ /// Find all providers matching a task name and caller data.
173173++ /// The caller_data is a map of field names to values for filter matching.
174174++ pub fn find_providers(
175175++ &self,
176176++ task_name: &str,
177177++ caller_data: &HashMap<String, serde_json::Value>,
178178++ ) -> Vec<&TaskProvider> {
179179++ let Some(providers) = self.providers.get(task_name) else {
180180++ return vec![];
181181++ };
182182++
183183++ providers
184184++ .iter()
185185++ .filter(|p| filters_match(&p.filters, caller_data))
186186++ .collect()
187187++ }
188188++
189189++ /// Get provider info suitable for sending to the embedder (chooser UI).
190190++ pub fn get_provider_infos(
191191++ &self,
192192++ task_name: &str,
193193++ caller_data: &HashMap<String, serde_json::Value>,
194194++ ) -> Vec<TaskProviderInfo> {
195195++ self.find_providers(task_name, caller_data)
196196++ .iter()
197197++ .enumerate()
198198++ .map(|(i, p)| {
199199++ let origin = p.href.origin().ascii_serialization();
200200++ TaskProviderInfo {
201201++ id: format!("{}:{}", task_name, i),
202202++ title: p.title.clone(),
203203++ description: p.description.clone(),
204204++ display: p.display.clone(),
205205++ icon: p.icon.clone(),
206206++ origin,
207207++ href: p.href.as_str().to_string(),
208208++ device_name: None,
209209++ remote_peer_id: None,
210210++ }
211211++ })
212212++ .collect()
213213++ }
214214++
215215++ /// Resolve a provider ID (e.g., "share:0") to the actual TaskProvider.
216216++ pub fn resolve_provider(&self, provider_id: &str) -> Option<&TaskProvider> {
217217++ let (task_name, index_str) = provider_id.rsplit_once(':')?;
218218++ let index: usize = index_str.parse().ok()?;
219219++ let providers = self.providers.get(task_name)?;
220220++ providers.get(index)
221221++ }
222222++
223223++ /// Save all providers to disk.
224224++ fn save(&self) {
225225++ let Some(path) = &self.config_path else {
226226++ return;
227227++ };
228228++ // Collect all providers into a flat list for serialization.
229229++ let all_providers: Vec<&TaskProvider> =
230230++ self.providers.values().flat_map(|v| v.iter()).collect();
231231++ match serde_json::to_string_pretty(&all_providers) {
232232++ Ok(json) => {
233233++ if let Err(e) = fs::write(path, json) {
234234++ error!("Failed to save task providers: {e}");
235235++ }
236236++ },
237237++ Err(e) => error!("Failed to serialize task providers: {e}"),
238238++ }
239239++ }
240240++
241241++ /// Load providers from disk.
242242++ fn load(&mut self) {
243243++ let Some(path) = &self.config_path else {
244244++ return;
245245++ };
246246++ let Ok(json) = fs::read_to_string(path) else {
247247++ return; // File doesn't exist yet, that's fine.
248248++ };
249249++ match serde_json::from_str::<Vec<TaskProvider>>(&json) {
250250++ Ok(providers) => {
251251++ for provider in providers {
252252++ self.providers
253253++ .entry(provider.task_name.clone())
254254++ .or_default()
255255++ .push(provider);
256256++ }
257257++ debug!(
258258++ "Loaded {} task providers from {}",
259259++ self.providers.values().map(|v| v.len()).sum::<usize>(),
260260++ path.display()
261261++ );
262262++ },
263263++ Err(e) => error!(
264264++ "Failed to parse task providers from {}: {e}",
265265++ path.display()
266266++ ),
267267++ }
268268++ }
269269++}
270270++
271271++/// A pending task request waiting for provider selection or result.
272272++pub struct PendingTaskRequest {
273273++ /// The task name.
274274++ pub task_name: String,
275275++ /// The caller's data (structured clone serialized).
276276++ pub data: Option<StructuredSerializedData>,
277277++ /// Callback to resolve the caller's promise with result data or error.
278278++ pub callback: GenericCallback<Result<StructuredSerializedData, String>>,
279279++ /// The selected provider (set after chooser selection).
280280++ pub provider: Option<TaskProvider>,
281281++ /// The virtual port ID for the provider side of the MessagePort pair.
282282++ pub provider_port_id: MessagePortId,
283283++ /// The URL the provider was opened with (set after provider selection).
284284++ /// Used to match new pipelines to pending task requests.
285285++ pub provider_url: Option<String>,
286286++ /// Whether the task data has been dispatched to the provider.
287287++ pub dispatched: bool,
288288++ /// The webview ID of the provider (set when the provider calls acceptTask).
289289++ pub provider_webview_id: Option<WebViewId>,
290290++ /// If this is a remote task, the peer ID of the requesting device.
291291++ /// When set, the result is sent back via P2P instead of resolving a local callback.
292292++ pub remote_caller_peer_id: Option<String>,
293293++ /// Remote providers discovered via P2P, keyed by provider ID.
294294++ pub remote_providers: HashMap<String, TaskProviderInfo>,
295295++}
296296++
297297++/// Check if all provider filters are satisfied by the caller's data.
298298++/// A provider with no filters matches everything.
299299++fn filters_match(
300300++ filters: &HashMap<String, FilterValue>,
301301++ caller_data: &HashMap<String, serde_json::Value>,
302302++) -> bool {
303303++ if filters.is_empty() {
304304++ return true;
305305++ }
306306++
307307++ for (field_name, filter) in filters {
308308++ let caller_value = caller_data.get(field_name);
309309++
310310++ match filter {
311311++ FilterValue::String(expected) => {
312312++ // Optional field, but if present must equal expected.
313313++ if let Some(val) = caller_value {
314314++ if val.as_str() != Some(expected.as_str()) {
315315++ return false;
316316++ }
317317++ }
318318++ },
319319++ FilterValue::Number(expected) => {
320320++ if let Some(val) = caller_value {
321321++ if val.as_f64() != Some(*expected) {
322322++ return false;
323323++ }
324324++ }
325325++ },
326326++ FilterValue::StringArray(allowed) => {
327327++ // Optional field, but if present must be one of allowed values.
328328++ if let Some(val) = caller_value {
329329++ let val_str = val.as_str().unwrap_or_default();
330330++ if !allowed.iter().any(|a| a == val_str) {
331331++ return false;
332332++ }
333333++ }
334334++ },
335335++ FilterValue::Object(obj) => {
336336++ if !filter_object_matches(obj, caller_value) {
337337++ return false;
338338++ }
339339++ },
340340++ }
341341++ }
342342++
343343++ true
344344++}
345345++
346346++/// Check if a filter definition object matches a caller's value.
347347++fn filter_object_matches(filter: &FilterObject, caller_value: Option<&serde_json::Value>) -> bool {
348348++ // Check required.
349349++ if filter.required && caller_value.is_none() {
350350++ return false;
351351++ }
352352++
353353++ let Some(val) = caller_value else {
354354++ // Not required and not present — passes.
355355++ return true;
356356++ };
357357++
358358++ // Check value constraint.
359359++ if let Some(allowed) = &filter.value {
360360++ let val_str = val.as_str().unwrap_or_default();
361361++ let matches = match allowed {
362362++ FilterAllowedValues::Single(s) => val_str == s,
363363++ FilterAllowedValues::Multiple(arr) => arr.iter().any(|a| a == val_str),
364364++ };
365365++ if !matches {
366366++ return false;
367367++ }
368368++ }
369369++
370370++ // Check min/max for numeric values.
371371++ if let Some(val_num) = val.as_f64() {
372372++ if let Some(min) = filter.min {
373373++ if val_num < min {
374374++ return false;
375375++ }
376376++ }
377377++ if let Some(max) = filter.max {
378378++ if val_num > max {
379379++ return false;
380380++ }
381381++ }
382382++ }
383383++
384384++ // Check pattern.
385385++ if let Some(pattern) = &filter.pattern {
386386++ let val_str = val.as_str().unwrap_or_default();
387387++ let flags = filter.pattern_flags.as_deref().unwrap_or("");
388388++ let regex_str = if flags.contains('i') {
389389++ format!("(?i){pattern}")
390390++ } else {
391391++ pattern.clone()
392392++ };
393393++ if let Ok(re) = regex::Regex::new(®ex_str) {
394394++ if !re.is_match(val_str) {
395395++ return false;
396396++ }
397397++ }
398398++ }
399399++
400400++ true
401401++}
402402++
403403++#[cfg(test)]
404404++mod tests {
405405++ use super::*;
406406++
407407++ #[test]
408408++ fn test_empty_filters_match_everything() {
409409++ let filters = HashMap::new();
410410++ let data = HashMap::new();
411411++ assert!(filters_match(&filters, &data));
412412++ }
413413++
414414++ #[test]
415415++ fn test_string_filter() {
416416++ let mut filters = HashMap::new();
417417++ filters.insert("type".into(), FilterValue::String("url".into()));
418418++
419419++ let mut data = HashMap::new();
420420++ data.insert("type".into(), serde_json::json!("url"));
421421++ assert!(filters_match(&filters, &data));
422422++
423423++ data.insert("type".into(), serde_json::json!("text"));
424424++ assert!(!filters_match(&filters, &data));
425425++
426426++ // Field not present — passes (optional).
427427++ let empty_data = HashMap::new();
428428++ assert!(filters_match(&filters, &empty_data));
429429++ }
430430++
431431++ #[test]
432432++ fn test_array_filter() {
433433++ let mut filters = HashMap::new();
434434++ filters.insert(
435435++ "type".into(),
436436++ FilterValue::StringArray(vec!["url".into(), "text".into()]),
437437++ );
438438++
439439++ let mut data = HashMap::new();
440440++ data.insert("type".into(), serde_json::json!("url"));
441441++ assert!(filters_match(&filters, &data));
442442++
443443++ data.insert("type".into(), serde_json::json!("image"));
444444++ assert!(!filters_match(&filters, &data));
445445++ }
446446++
447447++ #[test]
448448++ fn test_required_filter() {
449449++ let mut filters = HashMap::new();
450450++ filters.insert(
451451++ "url".into(),
452452++ FilterValue::Object(FilterObject {
453453++ required: true,
454454++ value: None,
455455++ min: None,
456456++ max: None,
457457++ pattern: None,
458458++ pattern_flags: None,
459459++ }),
460460++ );
461461++
462462++ let empty_data = HashMap::new();
463463++ assert!(!filters_match(&filters, &empty_data));
464464++
465465++ let mut data = HashMap::new();
466466++ data.insert("url".into(), serde_json::json!("https://example.com"));
467467++ assert!(filters_match(&filters, &data));
468468++ }
469469++
470470++ #[test]
471471++ fn test_pattern_filter() {
472472++ let mut filters = HashMap::new();
473473++ filters.insert(
474474++ "url".into(),
475475++ FilterValue::Object(FilterObject {
476476++ required: true,
477477++ value: None,
478478++ min: None,
479479++ max: None,
480480++ pattern: Some("^https://".into()),
481481++ pattern_flags: None,
482482++ }),
483483++ );
484484++
485485++ let mut data = HashMap::new();
486486++ data.insert("url".into(), serde_json::json!("https://example.com"));
487487++ assert!(filters_match(&filters, &data));
488488++
489489++ data.insert("url".into(), serde_json::json!("http://example.com"));
490490++ assert!(!filters_match(&filters, &data));
491491++ }
492492++
493493++ #[test]
494494++ fn test_min_max_filter() {
495495++ let mut filters = HashMap::new();
496496++ filters.insert(
497497++ "width".into(),
498498++ FilterValue::Object(FilterObject {
499499++ required: false,
500500++ value: None,
501501++ min: Some(100.0),
502502++ max: Some(1000.0),
503503++ pattern: None,
504504++ pattern_flags: None,
505505++ }),
506506++ );
507507++
508508++ let mut data = HashMap::new();
509509++ data.insert("width".into(), serde_json::json!(500));
510510++ assert!(filters_match(&filters, &data));
511511++
512512++ data.insert("width".into(), serde_json::json!(50));
513513++ assert!(!filters_match(&filters, &data));
514514++
515515++ data.insert("width".into(), serde_json::json!(1500));
516516++ assert!(!filters_match(&filters, &data));
517517++ }
518518++}
···108108 };
109109110110 self.pipeline_id.set(Some(new_pipeline_id));
111111-@@ -597,6 +636,148 @@
111111+@@ -597,6 +636,128 @@
112112 );
113113 }
114114···160160+ load_data.destination = Destination::IFrame;
161161+ load_data.policy_container = Some(window.as_global_scope().policy_container());
162162+
163163-+ // Clone load_data for spawning the pipeline later
164164-+ let load_data_for_spawn = load_data.clone();
165165-+ let theme = window.theme();
166166-+
167163+ // Get the iframe's size to use as the viewport for the embedded webview.
168164+ // We use border_box which gives us the size in CSS pixels.
169165+ let hidpi_scale_factor = window.device_pixel_ratio();
···188184+ ipc::channel().expect("Failed to create IPC channel for embedded webview");
189185+
190186+ let hide_focus = self.has_hide_focus();
187187++ let theme = window.theme();
191188+ let request = EmbeddedWebViewCreationRequest {
192189+ load_data,
193190+ parent_pipeline_id: pipeline_id,
···220217+ self.pipeline_id.set(Some(response.new_pipeline_id));
221218+ self.webview_id.set(Some(response.new_webview_id));
222219+
223223-+ // Spawn the pipeline in the script thread
224224-+ // Embedded webviews are top-level, so parent_info is None
225225-+ let new_pipeline_info = NewPipelineInfo {
226226-+ parent_info: None,
227227-+ new_pipeline_id: response.new_pipeline_id,
228228-+ browsing_context_id: response.new_browsing_context_id,
229229-+ webview_id: response.new_webview_id,
230230-+ opener: None,
231231-+ load_data: load_data_for_spawn,
232232-+ viewport_details,
233233-+ user_content_manager_id: None,
234234-+ theme,
235235-+ is_embedded_webview: true,
236236-+ hide_focus,
237237-+ };
238238-+
239239-+ with_script_thread(|script_thread| {
240240-+ script_thread.spawn_pipeline(new_pipeline_info);
241241-+ });
220220++ // The constellation already spawns the pipeline via Pipeline::spawn()
221221++ // in handle_create_embedded_webview, so we don't need to spawn it here.
242222+ },
243223+ Ok(None) => {
244224+ warn!("Embedded webview creation was rejected by embedder");
···257237 fn destroy_nested_browsing_context(&self) {
258238 self.pipeline_id.set(None);
259239 self.pending_pipeline_id.set(None);
260260-@@ -659,6 +840,13 @@
240240+@@ -659,6 +820,13 @@
261241 lazy_load_resumption_steps: Default::default(),
262242 pending_navigation: Default::default(),
263243 already_fired_synchronous_load_event: Default::default(),
···271251 }
272252 }
273253274274-@@ -694,7 +882,158 @@
254254+@@ -694,6 +862,157 @@
275255 self.webview_id.get()
276256 }
277257···376356+
377357+ /// Returns true if this iframe is hosting an embedded webview (created with "embed" attribute).
378358+ /// Embedded webviews have their own top-level WebViewId and window.parent === window.self.
379379- #[inline]
359359++ #[inline]
380360+ pub(crate) fn is_embedded_webview(&self) -> bool {
381361+ self.is_embedded_webview.get()
382362+ }
···426406+ self.page_zoom.set(zoom);
427407+ }
428408+
429429-+ #[inline]
409409+ #[inline]
430410 pub(crate) fn sandboxing_flag_set(&self) -> SandboxingFlagSet {
431411 self.sandboxing_flag_set
432432- .get()
433433-@@ -1078,6 +1417,89 @@
412412+@@ -1078,6 +1397,89 @@
434413435414 // https://html.spec.whatwg.org/multipage/#dom-iframe-longdesc
436415 make_url_setter!(SetLongDesc, "longdesc");
···520499 }
521500522501 impl VirtualMethods for HTMLIFrameElement {
523523-@@ -1134,10 +1556,40 @@
502502+@@ -1133,9 +1535,54 @@
503503+ // may be in a different script thread. Instead, we check to see if the parent
524504 // is in a document tree and has a browsing context, which is what causes
525505 // the child browsing context to be created.
526526- if self.upcast::<Node>().is_connected_with_browsing_context() {
527527-- debug!("iframe src set while in browsing context.");
528528-- self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx);
506506++
507507++ // Only process if the value actually changed (not just re-set to the same value).
508508++ let value_changed = match mutation {
509509++ AttributeMutation::Set(old_value, _) => {
510510++ old_value.map_or(true, |old| **old != **attr.value())
511511++ },
512512++ AttributeMutation::Removed => true,
513513++ };
514514++
515515++ if value_changed && self.upcast::<Node>().is_connected_with_browsing_context() {
529516+ // For embedded webviews, navigate using the load() method instead of
530517+ // processing iframe attributes (which is for regular nested iframes).
531518+ if self.is_embedded_webview.get() {
519519++ // Skip the initial src set (old=None) since create_embedded_webview
520520++ // already navigated. Only re-navigate on actual src changes.
521521++ let is_initial_set = matches!(mutation, AttributeMutation::Set(None, _));
522522++ if is_initial_set {
523523++ return;
524524++ }
532525+ if let Some(webview_id) = self.embedded_webview_id.get() {
533526+ let url = self
534527+ .shared_attribute_processing_steps_for_iframe_and_frame_elements(
···548541+ debug!("iframe src set while in browsing context.");
549542+ self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx);
550543+ }
551551- }
552552- },
544544++ }
545545++ },
553546+ local_name!("embed") => {
554547+ // The embed attribute determines whether this iframe hosts an embedded webview.
555548+ // Warn if it's changed after the iframe is already connected, as this is not supported.
556556-+ if self.upcast::<Node>().is_connected_with_browsing_context() {
549549+ if self.upcast::<Node>().is_connected_with_browsing_context() {
550550+- debug!("iframe src set while in browsing context.");
551551+- self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, cx);
557552+ warn!(
558553+ "The 'embed' attribute on iframe should not be changed after insertion. \
559554+ The iframe mode (nested vs embedded webview) is determined at insertion time."
560555+ );
561561-+ }
562562-+ },
556556+ }
557557+ },
563558 local_name!("loading") => {
564564- // https://html.spec.whatwg.org/multipage/#attr-iframe-loading
565565- // > When the loading attribute's state is changed to the Eager state, the user agent must run these steps:
566566-@@ -1200,6 +1652,23 @@
559559+@@ -1200,6 +1647,23 @@
567560568561 debug!("<iframe> running post connection steps");
569562···587580 // Step 1. Create a new child navigable for insertedNode.
588581 self.create_nested_browsing_context(cx);
589582590590-@@ -1223,11 +1692,25 @@
583583+@@ -1223,11 +1687,25 @@
591584 fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
592585 self.super_type().unwrap().unbind_from_tree(context, can_gc);
593586
···11--- original
22+++ modified
33-@@ -0,0 +1,37 @@
33+@@ -0,0 +1,62 @@
44+/* SPDX Id: AGPL-3.0-or-later */
55+
66+// Servo-specific API for communication between web content and the embedder.
···2525+ // This allows the window to be resized from web content.
2626+ undefined startWindowResize();
2727+
2828++ // Respond to a web task request with the selected provider id, or null to cancel.
2929++ undefined respondToTaskRequest(DOMString requestId, DOMString? providerId);
3030++
3131++ // Register a task provider. Only available to privileged pages.
3232++ undefined registerTaskProvider(TaskProviderDescriptor descriptor);
3333++
2834+ // Event fired when Servo encounters an error.
2935+ // The event is a CustomEvent with detail: { errorType: string, message: string }
3036+ attribute EventHandler onservoerror;
···3238+ // Event fired when a preference changes.
3339+ // The event is a CustomEvent with detail: { name: string, value: any }
3440+ attribute EventHandler onpreferencechanged;
4141++
4242++ // Event fired when a page requests a web task delegation.
4343++ // The event is a CustomEvent with detail: { taskName: string, providers: array, respond: function }
4444++ attribute EventHandler ontaskrequest;
4545++
4646++ // Event fired when additional remote task providers are discovered.
4747++ // The event is a CustomEvent with detail: { requestId: string, providers: string (JSON) }
4848++ attribute EventHandler ontaskprovidersupdate;
3549+};
3650+
3751+partial interface Navigator {
3852+ [Func="Embedder::is_allowed_to_embed"]
3953+ readonly attribute Embedder embedder;
4054+};
5555++
5656++dictionary TaskProviderDescriptor {
5757++ required DOMString name;
5858++ required DOMString title;
5959++ DOMString description;
6060++ required USVString href;
6161++ DOMString filters = "{}";
6262++ DOMString returns;
6363++ DOMString display = "window";
6464++ USVString icon;
6565++};
···11+--- original
22++++ modified
33+@@ -0,0 +1,26 @@
44++/* SPDX-License-Identifier: AGPL-3.0-or-later */
55++
66++/// Web Tasks API — allows any page to request a task and have the system
77++/// find and launch a suitable provider.
88++///
99++/// The options parameter is a plain JS object whose properties are:
1010++/// - Matched against provider filters for provider selection
1111++/// - Passed as-is to the provider via acceptTask()
1212++///
1313++/// Example: navigator.requestTask("share", { type: "url", url: "https://...", text: "Hi" })
1414++
1515++partial interface Navigator {
1616++ [Throws] Promise<any> requestTask(DOMString name, optional any options);
1717++};
1818++
1919++/// Data returned by window.acceptTask().
2020++dictionary TaskStartData {
2121++ required DOMString name;
2222++ any data;
2323++ required MessagePort port;
2424++};
2525++
2626++partial interface Window {
2727++ /// Called by a task provider to accept and receive the task.
2828++ [Throws] Promise<TaskStartData> acceptTask();
2929++};
···209209 /// Mark a new document as active
210210 ActivateDocument,
211211 /// Set the document state for a pipeline (used by screenshot / reftests)
212212-@@ -722,6 +855,79 @@
212212+@@ -722,6 +855,109 @@
213213 RespondToScreenshotReadinessRequest(ScreenshotReadinessResponse),
214214 /// Request the constellation to force garbage collection in all `ScriptThread`'s.
215215 TriggerGarbageCollection,
···286286+ PeerStreamResponse(String, String, bool),
287287+ /// ATProto api message.
288288+ AtProto(AtProtoRequest, GenericCallback<AtProtoResult>),
289289++ /// Request a web task delegation. The constellation will find matching providers,
290290++ /// show a chooser, launch the selected provider, and return the result.
291291++ /// Args: task_name, caller_data_json (for filter matching), display_preference, data (structured clone), provider_port_id, callback.
292292++ RequestTask(
293293++ String,
294294++ String,
295295++ Option<String>,
296296++ Option<StructuredSerializedData>,
297297++ MessagePortId,
298298++ GenericCallback<Result<StructuredSerializedData, String>>,
299299++ ),
300300++ /// Register a task provider.
301301++ /// Args: task_name, title, description, href, filters_json, returns_type, display_mode, icon.
302302++ RegisterTaskProvider(
303303++ String,
304304++ String,
305305++ Option<String>,
306306++ String,
307307++ String,
308308++ Option<String>,
309309++ String,
310310++ Option<String>,
311311++ ),
312312++ /// User selected a task provider (or cancelled). Sent by the system UI's
313313++ /// respondToTaskRequest(). Args: request_id, provider_id (None = cancelled).
314314++ TaskProviderSelected(String, Option<String>),
315315++ /// A task provider page calls window.acceptTask() to receive its task.
316316++ /// Constellation responds with the task data via callback.
317317++ /// Args: callback(task_name, data, provider_port_id_bytes).
318318++ AcceptTask(GenericCallback<Option<(String, Option<StructuredSerializedData>, Vec<u8>)>>),
289319 }
290320291321 impl fmt::Debug for ScriptToConstellationMessage {
+17-1
patches/components/shared/script/lib.rs.patch
···3535 }
36363737 /// When a pipeline is closed, should its browsing context be discarded too?
3838-@@ -322,6 +330,24 @@
3838+@@ -286,6 +294,15 @@
3939+ SendImageKeysBatch(PipelineId, Vec<ImageKey>),
4040+ /// Preferences were updated in the parent process.
4141+ PreferencesUpdated(Vec<(String, PrefValue)>),
4242++ /// A web task request needs to be shown to the system UI.
4343++ /// Fields: request_id, task_name, providers_json.
4444++ ShowTaskChooser(String, String, String),
4545++ /// Tell the system UI to open a task provider webview.
4646++ /// Fields: request_id, provider_url, provider_title.
4747++ OpenTaskProvider(String, String, String),
4848++ /// Update the system UI with additional remote task providers.
4949++ /// Fields: request_id, providers_json.
5050++ TaskProvidersUpdate(String, String),
5151+ /// Notify the `ScriptThread` that the Servo renderer is no longer waiting on
5252+ /// asynchronous image uploads for the given `Pipeline`. These are mainly used
5353+ /// by canvas to perform uploads while the display list is being built.
5454+@@ -322,6 +339,24 @@
3955 SetAccessibilityActive(PipelineId, bool),
4056 /// Force a garbage collection in this script thread.
4157 TriggerGarbageCollection,