From 84e06e94227ecac8c075c7a317c4cd861dce9f9c Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Thu, 20 Aug 2026 23:28:13 -0700 Subject: [PATCH 1/5] feat(remote-ssh): forward remote ports to this machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starting a server on an SSH host left it unreachable from here. `PortForwardManager` existed but was a placeholder: it recorded forwards in a HashMap and logged "not fully implemented - connections will not be forwarded". Implement local (`-L`) forwarding for real. Each forward binds a listener and splices accepted connections onto a `direct-tcpip` channel, so the remote sshd dials the service and both directions are copied. The remote endpoint is a forward's identity; the local port is only an allocation. A requested local port that is taken is replaced rather than refused, and the port originally asked for comes back with the mapping so the UI can say the address moved instead of silently handing over a different one. Privileged ports fall back the same way. Three failure modes worth naming: - Channels are opened through SSHConnectionManager per connection instead of against a cached handle, so a forward survives an automatic reconnect. That same path would let a forward nobody stopped dial back out to a host the user explicitly disconnected from, so each connection checks liveness first, and disconnect tears down the forwards it carried. - A local Docker workspace has no SSH session to carry a channel. It is rejected when the mapping is added rather than on the first request. - Forwards bind loopback unless the user opts into exposing them on the local network. Add remote listening-port discovery (ss, then lsof, then netstat, with a marker line naming the dialect) so a port can be picked rather than guessed. Discovery forwards nothing on its own. The UI lives on the `···` menu of a remote workspace item, because a forward is carried by that workspace's SSH session. Detection runs when the dialog opens and its results lead: a port the remote is listening on is one click from being forwarded, instead of a number to go look up and type. Nothing is forwarded automatically — a click is still a request — and a manual row stays for a specific local port or a host detection cannot see. Add a PortForward Agent tool alongside it. It is not confined to remote-SSH workspaces: users often stay in a local workspace and tell an Agent to ssh somewhere and start a server, and that Agent needs the same capability. `target` accepts whatever name the caller already has for the host — a saved connection, a `~/.ssh/config` alias, or `[user@]host[:port]` — and is optional in a remote workspace, where it defaults to that workspace's connection. A `targets` operation lists what is reachable. Ad-hoc targets authenticate with an agent or a config-declared key, since no one can type a passphrase into a tool call. Both surfaces share one registry through a process-global manager, so a forward made by hand and one made by an Agent are the same object and either side can list or stop the other's. Reverse forwarding keeps its entry point but now fails loudly; it needs `tcpip-forward` global requests and a `forwarded-tcpip` handler that the session handler does not carry. --- src/apps/desktop/src/api/app_state.rs | 15 +- src/apps/desktop/src/api/ssh_api.rs | 81 +- src/apps/desktop/src/lib.rs | 5 + .../src/agentic/tools/implementations/mod.rs | 6 +- .../implementations/port_forward_tool.rs | 670 ++++++++++ .../tools/product_runtime/materialization.rs | 2 + .../core/src/service/remote_ssh/mod.rs | 11 +- .../execution/tool-provider-groups/src/lib.rs | 13 +- .../src/remote_ssh/disabled.rs | 57 +- .../src/remote_ssh/manager.rs | 232 +--- .../src/remote_ssh/mod.rs | 16 +- .../src/remote_ssh/port_forward.rs | 1099 +++++++++++++++++ .../src/remote_ssh/types.rs | 133 ++ .../remote_ssh_contracts.rs | 85 +- .../sections/workspaces/WorkspaceItem.tsx | 76 +- .../ssh-remote/PortForwardDialog.scss | 269 ++++ .../ssh-remote/PortForwardDialog.test.tsx | 330 +++++ .../features/ssh-remote/PortForwardDialog.tsx | 492 ++++++++ .../src/features/ssh-remote/appearance.ts | 4 + src/web-ui/src/features/ssh-remote/index.ts | 1 + src/web-ui/src/features/ssh-remote/sshApi.ts | 37 + src/web-ui/src/features/ssh-remote/types.ts | 42 + src/web-ui/src/locales/en-US/common.json | 33 + src/web-ui/src/locales/zh-CN/common.json | 33 + src/web-ui/src/locales/zh-TW/common.json | 33 + 25 files changed, 3554 insertions(+), 221 deletions(-) create mode 100644 src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs create mode 100644 src/crates/services/services-integrations/src/remote_ssh/port_forward.rs create mode 100644 src/web-ui/src/features/ssh-remote/PortForwardDialog.scss create mode 100644 src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx create mode 100644 src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index fcd1d21b1f..1fedc9d3f4 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -8,7 +8,8 @@ use bitfun_core::miniapp::{ initialize_global_miniapp_manager, seed_builtin_miniapps, JsWorkerPool, MiniAppManager, }; use bitfun_core::service::remote_ssh::{ - init_remote_workspace_manager, RemoteFileService, RemoteTerminalManager, SSHConnectionManager, + global_port_forward_manager, init_remote_workspace_manager, PortForwardManager, + RemoteFileService, RemoteTerminalManager, SSHConnectionManager, }; use bitfun_core::service::{announcement, config, filesystem, mcp, search, token_usage, workspace}; use bitfun_core::util::errors::*; @@ -84,6 +85,10 @@ pub struct AppState { pub ssh_manager: Arc>>, pub remote_file_service: Arc>>, pub remote_terminal_manager: Arc>>, + /// Local (`-L`) port forwards. Unlike the services above this needs no + /// `Option`: it is useful before any connection exists, and its own entry + /// points report a missing SSH manager. + pub port_forward_manager: PortForwardManager, pub remote_workspace: Arc>>, pub active_searches: Arc>>>, /// Cancellation flags for active file transfers (download/upload), keyed by transfer_id. @@ -274,6 +279,13 @@ impl AppState { let manager_for_fs = Arc::new(tokio::sync::RwLock::new(Some(manager_arc.as_ref().clone()))); let fs = RemoteFileService::new(manager_for_fs.clone()); let tm = RemoteTerminalManager::new(manager_arc.as_ref().clone()); + // Share the process-wide registry rather than owning a private one: the + // Agent tool reaches forwards through the same global, and a mapping + // made in the UI has to be visible to the Agent and vice versa. + let port_forward_manager = global_port_forward_manager(); + port_forward_manager + .set_ssh_manager(manager_arc.as_ref().clone()) + .await; // Clone for storing in AppState let fs_for_state = fs.clone(); @@ -316,6 +328,7 @@ impl AppState { ssh_manager, remote_file_service, remote_terminal_manager, + port_forward_manager, remote_workspace, active_searches: Arc::new(Mutex::new(HashMap::new())), active_transfers: Arc::new(Mutex::new(HashMap::new())), diff --git a/src/apps/desktop/src/api/ssh_api.rs b/src/apps/desktop/src/api/ssh_api.rs index 779bf38207..24223bdaa0 100644 --- a/src/apps/desktop/src/api/ssh_api.rs +++ b/src/apps/desktop/src/api/ssh_api.rs @@ -11,7 +11,8 @@ use crate::api::app_state::SSHServiceError; use crate::startup_trace::DesktopStartupTrace; use crate::AppState; use bitfun_core::service::remote_ssh::{ - ConnectionTestReport, DockerContainerInfo, RemoteTreeNode, SSHAuthMethod, SSHConfigEntry, + list_remote_listening_ports, ConnectionTestReport, DockerContainerInfo, PortForward, + PortForwardRequest, RemoteListeningPort, RemoteTreeNode, SSHAuthMethod, SSHConfigEntry, SSHConfigLookupResult, SSHConnectionConfig, SSHConnectionManager, SSHConnectionResult, SavedConnection, ServerInfo, }; @@ -188,6 +189,13 @@ pub async fn ssh_disconnect( connection_id: String, ) -> Result<(), String> { let manager = state.get_ssh_manager_async().await?; + // Tear the forwards down first. They are listeners whose only meaning is + // this session, and leaving one running would let a later connection on it + // reconnect the host the user just closed. + state + .port_forward_manager + .stop_for_connection(&connection_id) + .await; manager .disconnect(&connection_id) .await @@ -197,6 +205,7 @@ pub async fn ssh_disconnect( #[tauri::command] pub async fn ssh_disconnect_all(state: State<'_, AppState>) -> Result<(), String> { let manager = state.get_ssh_manager_async().await?; + state.port_forward_manager.stop_all().await; manager.disconnect_all().await; Ok(()) } @@ -1120,6 +1129,76 @@ pub async fn remote_get_workspace_info( Ok(workspace) } +// === Port Forwarding === + +/// Start a local (`-L`) forward and return the mapping that was established. +/// +/// The returned `localPort` is not always the requested one: an unavailable +/// port is replaced rather than refused, and `requestedLocalPort` carries what +/// was asked for so the UI can say so. +#[tauri::command] +pub async fn ssh_start_port_forward( + state: State<'_, AppState>, + request: PortForwardRequest, +) -> Result { + log::info!( + "ssh_start_port_forward: connection={}, remote={}:{}, requested local port={:?}", + request.connection_id, + request.effective_remote_host(), + request.remote_port, + request.preferred_local_port() + ); + state + .port_forward_manager + .start_local_forward(&request) + .await + .map_err(|error| format!("{error:#}")) +} + +#[tauri::command] +pub async fn ssh_stop_port_forward( + state: State<'_, AppState>, + forward_id: String, +) -> Result<(), String> { + state + .port_forward_manager + .stop_forward(&forward_id) + .await + .map_err(|error| format!("{error:#}")) +} + +/// List forwards, optionally narrowed to one connection. +#[tauri::command] +pub async fn ssh_list_port_forwards( + state: State<'_, AppState>, + connection_id: Option, +) -> Result, String> { + Ok(match connection_id { + Some(connection_id) => { + state + .port_forward_manager + .list_forwards_for_connection(&connection_id) + .await + } + None => state.port_forward_manager.list_forwards().await, + }) +} + +/// List the TCP ports currently accepting connections on the remote host. +/// +/// Discovery only. Nothing is forwarded as a result of calling this; it exists +/// so users can pick a real port instead of guessing one. +#[tauri::command] +pub async fn ssh_list_remote_listening_ports( + state: State<'_, AppState>, + connection_id: String, +) -> Result, String> { + let manager = state.get_ssh_manager_async().await?; + list_remote_listening_ports(&manager, &connection_id) + .await + .map_err(|error| format!("{error:#}")) +} + #[cfg(test)] mod tests { use super::{ diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 131d36d5af..34376fd767 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -1886,6 +1886,11 @@ pub async fn run() { api::ssh_api::remote_close_workspace, api::ssh_api::remote_remove_workspace, api::ssh_api::remote_get_workspace_info, + // Remote port forwarding (local `-L` mappings, user-driven) + api::ssh_api::ssh_start_port_forward, + api::ssh_api::ssh_stop_port_forward, + api::ssh_api::ssh_list_port_forwards, + api::ssh_api::ssh_list_remote_listening_ports, // Detached task dispatch (controller-side SSH transport) api::dispatch_api::dispatch_list_targets, api::dispatch_api::dispatch_probe_target, diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/mod.rs b/src/crates/assembly/core/src/agentic/tools/implementations/mod.rs index 78f2dc321c..23836bafde 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/mod.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/mod.rs @@ -56,6 +56,8 @@ pub mod page_publish_tool; #[cfg(feature = "tools-miniapp")] pub mod playbook_tool; #[cfg(feature = "tools-git")] +pub mod port_forward_tool; +#[cfg(feature = "tools-git")] pub mod review_platform_tool; pub mod session_control_tool; pub mod session_history_tool; @@ -71,7 +73,6 @@ pub mod util; pub mod view_image_tool; #[cfg(feature = "tools-browser-web")] pub mod web; -#[cfg(feature = "tools-git")] pub mod worktree_tool; #[deprecated(note = "GetToolSpecTool is owned by the product tool runtime boundary")] @@ -128,6 +129,8 @@ pub use page_publish_tool::PagePublishTool; #[cfg(feature = "tools-miniapp")] pub use playbook_tool::PlaybookTool; #[cfg(feature = "tools-git")] +pub use port_forward_tool::PortForwardTool; +#[cfg(feature = "tools-git")] pub use review_platform_tool::ReviewPlatformTool; pub use session_control_tool::SessionControlTool; pub use session_history_tool::SessionHistoryTool; @@ -143,5 +146,4 @@ pub use todo_write_tool::TodoWriteTool; pub use view_image_tool::ViewImageTool; #[cfg(feature = "tools-browser-web")] pub use web::{WebFetchTool, WebSearchTool}; -#[cfg(feature = "tools-git")] pub use worktree_tool::WorktreeTool; diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs new file mode 100644 index 0000000000..b0f87f5366 --- /dev/null +++ b/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs @@ -0,0 +1,670 @@ +//! Agent tool for making a port on an SSH host reachable from this machine. +//! +//! Works from any session, not just a remote-SSH workspace. `target` accepts +//! whatever the caller already knows the host as — a saved BitFun connection, +//! a `~/.ssh/config` alias, or `[user@]host[:port]` — because an Agent that +//! reached a box with `ssh myserver` in a shell should be able to forward its +//! ports with the same name it just used. When the session *is* bound to a +//! remote workspace, `target` can be omitted and that connection is used. + +use crate::agentic::tools::framework::{ + PermissionIntent, Tool, ToolExposure, ToolRenderOptions, ToolResult, ToolUseContext, + ValidationResult, +}; +use crate::service::remote_ssh::{ + global_port_forward_manager, list_remote_listening_ports, PortForwardRequest, SSHAuthMethod, + SSHConnectionConfig, SSHConnectionManager, SSHConnectionOptions, +}; +use crate::util::errors::{BitFunError, BitFunResult}; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::{json, Value}; + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum PortForwardOperation { + Targets, + Detect, + Start, + List, + Stop, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +struct PortForwardToolInput { + operation: PortForwardOperation, + #[serde(default)] + target: Option, + #[serde(default)] + remote_port: Option, + #[serde(default)] + remote_host: Option, + #[serde(default)] + local_port: Option, + #[serde(default)] + expose_on_lan: bool, + #[serde(default)] + label: Option, + #[serde(default)] + forward_id: Option, +} + +impl PortForwardToolInput { + fn trimmed_target(&self) -> Option<&str> { + self.target + .as_deref() + .map(str::trim) + .filter(|target| !target.is_empty()) + } +} + +/// An SSH endpoint parsed out of a `[user@]host[:port]` string. +struct ParsedEndpoint { + user: Option, + host: String, + port: Option, +} + +fn parse_endpoint(target: &str) -> Option { + let (user, rest) = match target.rsplit_once('@') { + Some((user, rest)) if !user.is_empty() && !rest.is_empty() => { + (Some(user.to_string()), rest) + } + Some(_) => return None, + None => (None, target), + }; + // `host:port`, but never mistake an IPv6 literal's colons for a port. + let (host, port) = match rest.rsplit_once(':') { + Some((host, port)) if !host.contains(':') && !host.is_empty() => { + match port.parse::() { + Ok(port) => (host, Some(port)), + Err(_) => return None, + } + } + _ => (rest, None), + }; + let host = host.trim_matches(|c| c == '[' || c == ']'); + if host.is_empty() || host.contains(char::is_whitespace) { + return None; + } + Some(ParsedEndpoint { + user, + host: host.to_string(), + port, + }) +} + +fn current_username() -> String { + std::env::var("USER") + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_else(|_| "root".to_string()) +} + +pub struct PortForwardTool; + +impl PortForwardTool { + pub fn new() -> Self { + Self + } + + /// Connection bound to this session, when there is one. + fn workspace_connection_id(context: &ToolUseContext) -> Option { + let workspace = context.workspace.as_ref()?; + if !workspace.is_remote() { + return None; + } + workspace.connection_id().map(str::to_string) + } + + /// Turn whatever the caller called the host into a live connection id. + /// + /// Saved connections win over `~/.ssh/config` so an explicitly configured + /// profile (with its stored credentials) is preferred over re-deriving one, + /// and a bare endpoint is the last resort. + async fn resolve_target( + manager: &SSHConnectionManager, + target: Option<&str>, + context: &ToolUseContext, + ) -> BitFunResult { + let Some(target) = target else { + return Self::workspace_connection_id(context).ok_or_else(|| { + BitFunError::tool( + "This session is not bound to a remote SSH workspace. Pass `target` with a \ + saved connection, an ~/.ssh/config host, or [user@]host[:port]. Use \ + operation \"targets\" to see what is available." + .to_string(), + ) + }); + }; + + let saved = manager.get_saved_connections().await; + if let Some(found) = saved + .iter() + .find(|connection| connection.id == target) + .or_else(|| { + saved + .iter() + .find(|connection| connection.name.eq_ignore_ascii_case(target)) + }) + .or_else(|| { + saved + .iter() + .find(|connection| connection.host.eq_ignore_ascii_case(target)) + }) + { + manager + .ensure_connected(&found.id) + .await + .map_err(|error| BitFunError::tool(format!("{error:#}")))?; + return Ok(found.id.clone()); + } + + Self::connect_ad_hoc(manager, target).await + } + + /// Build a connection from `~/.ssh/config` or a bare endpoint and open it. + /// + /// The id is derived from the endpoint so repeated calls reuse one session + /// instead of stacking a new one per tool call. Nothing is persisted to the + /// user's saved connections. + async fn connect_ad_hoc(manager: &SSHConnectionManager, target: &str) -> BitFunResult { + let endpoint = parse_endpoint(target).ok_or_else(|| { + BitFunError::tool(format!( + "'{target}' is not a saved connection, an ~/.ssh/config host, or [user@]host[:port]" + )) + })?; + + let lookup = manager.get_ssh_config(&endpoint.host).await; + let config_entry = lookup.config; + + let host = config_entry + .as_ref() + .and_then(|entry| entry.hostname.clone()) + .unwrap_or_else(|| endpoint.host.clone()); + let port = endpoint + .port + .or_else(|| config_entry.as_ref().and_then(|entry| entry.port)) + .unwrap_or(22); + let username = endpoint + .user + .clone() + .or_else(|| config_entry.as_ref().and_then(|entry| entry.user.clone())) + .unwrap_or_else(current_username); + + // Only non-interactive auth can work here: there is no one to type a + // passphrase into a tool call. + let auth = match config_entry + .as_ref() + .and_then(|entry| entry.identity_file.clone()) + { + Some(key_path) => SSHAuthMethod::PrivateKey { + key_path, + passphrase: None, + certificate_path: config_entry + .as_ref() + .and_then(|entry| entry.certificate_file.clone()), + }, + None => SSHAuthMethod::Agent { + key_fingerprint: None, + fallback_key_path: None, + }, + }; + + let connection_id = format!("port-forward:{username}@{host}:{port}"); + if manager.is_connected(&connection_id).await { + return Ok(connection_id); + } + + let result = manager + .connect(SSHConnectionConfig { + id: connection_id.clone(), + name: target.to_string(), + host, + port, + username, + auth, + default_workspace: None, + proxy_jump: config_entry.and_then(|entry| entry.proxy_jump), + container: None, + options: SSHConnectionOptions::default(), + }) + .await + .map_err(|error| BitFunError::tool(format!("{error:#}")))?; + + if !result.success { + return Err(BitFunError::tool(format!( + "Could not connect to '{target}': {}. Key or agent authentication is required; \ + a target that needs a password must be saved in BitFun's SSH connections first.", + result.error.unwrap_or_else(|| "unknown error".to_string()) + ))); + } + Ok(connection_id) + } + + fn require_remote_port(input: &PortForwardToolInput) -> BitFunResult { + input + .remote_port + .filter(|port| *port != 0) + .ok_or_else(|| BitFunError::tool("start requires remote_port".to_string())) + } + + fn require_forward_id(input: &PortForwardToolInput) -> BitFunResult { + input + .forward_id + .as_deref() + .map(str::trim) + .filter(|id| !id.is_empty()) + .map(str::to_string) + .ok_or_else(|| BitFunError::tool("stop requires forward_id".to_string())) + } +} + +impl Default for PortForwardTool { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Tool for PortForwardTool { + fn name(&self) -> &str { + "PortForward" + } + + async fn description(&self) -> BitFunResult { + Ok(r#"Make a port listening on an SSH host reachable from the user's machine, so they can open it in a browser. + +Operations: +- "targets": List SSH hosts you can forward from (saved BitFun connections and ~/.ssh/config hosts). Read-only. +- "detect": List TCP ports currently listening on the host. Read-only; forwards nothing. +- "start": Forward one remote port. Requires remote_port. Returns the local address to give the user. +- "list": Show forwards currently running for the host. +- "stop": Stop one forward by forward_id. + +`target` names the SSH host: a saved connection (id or name), a ~/.ssh/config Host, or [user@]host[:port]. Omit it only when the session already runs in a remote SSH workspace. Ad-hoc targets authenticate with an SSH agent or a config-declared key; a host needing a password must be saved in BitFun's SSH connections first. + +The returned local_port is authoritative and is NOT always the port requested: if that local port is in use, a free one is bound instead and requested_local_port records what was asked for. Always report the returned local_address rather than assuming it matches the remote port. + +Forwards bind 127.0.0.1 and last until stopped or the connection drops. Set expose_on_lan only when the user wants other devices on their network to reach the service too."# + .to_string()) + } + + fn short_description(&self) -> String { + "Forward a port from any SSH host to the user's machine.".to_string() + } + + fn default_exposure(&self) -> ToolExposure { + ToolExposure::Deferred + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": ["targets", "detect", "start", "list", "stop"] + }, + "target": { + "type": "string", + "description": "SSH host: a saved connection (id or name), a ~/.ssh/config Host, or [user@]host[:port]. Optional in a remote SSH workspace, where it defaults to that workspace's host." + }, + "remote_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Required for start. The port as seen from the SSH host." + }, + "remote_host": { + "type": "string", + "description": "Address the SSH host uses to reach the service. Defaults to 127.0.0.1, which is what a server bound to localhost needs. Set it to reach a service on another machine in the host's network." + }, + "local_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Preferred local port for start. Omit to get a free one; a requested port that is taken is replaced, not refused." + }, + "expose_on_lan": { + "type": "boolean", + "default": false, + "description": "Bind every local interface instead of 127.0.0.1, so other devices on the user's network can reach the service." + }, + "label": { + "type": "string", + "description": "Short name shown to the user, e.g. the service name." + }, + "forward_id": { + "type": "string", + "description": "Required for stop. Comes from start or list." + } + }, + "required": ["operation"], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn permission_intents( + &self, + input: &Value, + _context: &ToolUseContext, + ) -> BitFunResult> { + let input: PortForwardToolInput = serde_json::from_value(input.clone()) + .map_err(|error| BitFunError::validation(format!("Invalid input: {error}")))?; + let target = input.trimmed_target().unwrap_or("workspace").to_string(); + let intent = match input.operation { + // Reading what exists changes nothing on either machine. + PortForwardOperation::Targets + | PortForwardOperation::Detect + | PortForwardOperation::List => return Ok(Vec::new()), + PortForwardOperation::Start => PermissionIntent::new( + "port_forward.start", + vec![format!( + "{target}:{}", + input.remote_port.unwrap_or_default() + )], + ), + PortForwardOperation::Stop => PermissionIntent::new( + "port_forward.stop", + vec![format!( + "{target}:{}", + input.forward_id.as_deref().unwrap_or_default() + )], + ), + }; + Ok(vec![intent]) + } + + fn render_tool_use_message(&self, input: &Value, _options: &ToolRenderOptions) -> String { + let target = input + .get("target") + .and_then(Value::as_str) + .map(|target| format!(" on {target}")) + .unwrap_or_default(); + match input.get("operation").and_then(Value::as_str) { + Some("targets") => "Listing SSH hosts available for forwarding".to_string(), + Some("detect") => format!("Detecting listening ports{target}"), + Some("list") => format!("Listing port forwards{target}"), + Some("start") => match input.get("remote_port").and_then(Value::as_u64) { + Some(port) => format!("Forwarding port {port}{target}"), + None => format!("Starting a port forward{target}"), + }, + Some("stop") => "Stopping a port forward".to_string(), + _ => "Port forwarding".to_string(), + } + } + + async fn validate_input( + &self, + input: &Value, + _context: Option<&ToolUseContext>, + ) -> ValidationResult { + let invalid = |message: String| ValidationResult { + result: false, + message: Some(message), + error_code: Some(400), + meta: None, + }; + + let parsed: PortForwardToolInput = match serde_json::from_value(input.clone()) { + Ok(parsed) => parsed, + Err(error) => return invalid(format!("Invalid input: {error}")), + }; + match parsed.operation { + PortForwardOperation::Start => { + if let Err(error) = Self::require_remote_port(&parsed) { + return invalid(error.to_string()); + } + } + PortForwardOperation::Stop => { + if let Err(error) = Self::require_forward_id(&parsed) { + return invalid(error.to_string()); + } + } + _ => {} + } + + ValidationResult { + result: true, + message: None, + error_code: None, + meta: None, + } + } + + async fn call_impl( + &self, + input: &Value, + context: &ToolUseContext, + ) -> BitFunResult> { + let input: PortForwardToolInput = serde_json::from_value(input.clone()) + .map_err(|error| BitFunError::tool(format!("Invalid input: {error}")))?; + let ssh_manager = resolve_ssh_manager().await?; + let forwards = global_port_forward_manager(); + + // `stop` addresses a forward directly and `targets` has no host yet, so + // neither needs a connection resolved first. + let data = match input.operation { + PortForwardOperation::Targets => { + let saved = ssh_manager.get_saved_connections().await; + let mut targets = Vec::new(); + for connection in &saved { + targets.push(json!({ + "target": connection.id, + "name": connection.name, + "host": connection.host, + "port": connection.port, + "username": connection.username, + "source": "saved", + "connected": ssh_manager.is_connected(&connection.id).await, + })); + } + for entry in ssh_manager.list_ssh_config_hosts().await { + if saved + .iter() + .any(|connection| connection.host.eq_ignore_ascii_case(&entry.host)) + { + continue; + } + targets.push(json!({ + "target": entry.host, + "host": entry.hostname.unwrap_or_else(|| entry.host.clone()), + "port": entry.port, + "username": entry.user, + "source": "ssh_config", + "connected": false, + })); + } + json!({ + "success": true, + "operation": "targets", + "count": targets.len(), + "targets": targets, + "note": "Any [user@]host[:port] also works as a target, not only these.", + }) + } + PortForwardOperation::Detect => { + let connection_id = + Self::resolve_target(&ssh_manager, input.trimmed_target(), context).await?; + let ports = list_remote_listening_ports(&ssh_manager, &connection_id) + .await + .map_err(|error| BitFunError::tool(format!("{error:#}")))?; + json!({ + "success": true, + "operation": "detect", + "target": connection_id, + "count": ports.len(), + "listening_ports": ports, + }) + } + PortForwardOperation::Start => { + let remote_port = Self::require_remote_port(&input)?; + let connection_id = + Self::resolve_target(&ssh_manager, input.trimmed_target(), context).await?; + let forward = forwards + .start_local_forward(&PortForwardRequest { + connection_id: connection_id.clone(), + remote_port, + remote_host: input.remote_host.clone(), + local_port: input.local_port, + expose_on_lan: input.expose_on_lan, + label: input.label.clone(), + }) + .await + .map_err(|error| BitFunError::tool(format!("{error:#}")))?; + let moved = forward.requested_local_port.is_some(); + json!({ + "success": true, + "operation": "start", + "target": connection_id, + "local_address": forward.local_address(), + "local_url": format!("http://{}", forward.local_address()), + "local_port_moved": moved, + "forward": forward, + "note": if moved { + "The requested local port was unavailable, so a different one was bound. Report local_address to the user." + } else { + "Report local_address to the user." + }, + }) + } + PortForwardOperation::List => { + let connection_id = + Self::resolve_target(&ssh_manager, input.trimmed_target(), context).await?; + let active = forwards.list_forwards_for_connection(&connection_id).await; + json!({ + "success": true, + "operation": "list", + "target": connection_id, + "count": active.len(), + "forwards": active, + }) + } + PortForwardOperation::Stop => { + let forward_id = Self::require_forward_id(&input)?; + forwards + .stop_forward(&forward_id) + .await + .map_err(|error| BitFunError::tool(format!("{error:#}")))?; + json!({ + "success": true, + "operation": "stop", + "forward_id": forward_id, + }) + } + }; + + Ok(vec![ToolResult::Result { + result_for_assistant: Some(data.to_string()), + data, + image_attachments: None, + }]) + } +} + +/// Reach the SSH manager that owns this process's connections. +async fn resolve_ssh_manager() -> BitFunResult { + let manager = crate::service::remote_ssh::workspace_state::get_remote_workspace_manager() + .ok_or_else(|| BitFunError::tool("Remote SSH services are not initialized".to_string()))?; + manager + .get_ssh_manager() + .await + .ok_or_else(|| BitFunError::tool("SSH manager is not available".to_string())) +} + +#[cfg(test)] +mod tests { + use super::{parse_endpoint, PortForwardTool}; + use crate::agentic::tools::framework::Tool; + use serde_json::json; + + #[test] + fn schema_stays_flat_and_covers_every_operation() { + let tool = PortForwardTool::new(); + let schema = tool.input_schema(); + let properties = schema["properties"] + .as_object() + .expect("properties should be an object"); + + // A flat, small property set is what keeps this callable in one shot. + assert!( + properties.len() <= 8, + "schema grew to {} properties", + properties.len() + ); + assert_eq!( + schema["required"].as_array().map(Vec::len), + Some(1), + "operation should be the only required field" + ); + + let operations = schema["properties"]["operation"]["enum"] + .as_array() + .expect("operation enum"); + for operation in ["targets", "detect", "start", "list", "stop"] { + assert!( + operations.contains(&json!(operation)), + "missing {operation}" + ); + } + } + + #[tokio::test] + async fn the_tool_is_offered_outside_remote_workspaces() { + // A session that reached a host with `ssh` in a shell still needs to + // forward its ports, so availability must not depend on the workspace. + let tool = PortForwardTool::new(); + assert!(tool.is_available_in_context(None).await); + } + + #[tokio::test] + async fn start_and_stop_report_their_missing_field() { + let tool = PortForwardTool::new(); + assert!( + !tool + .validate_input(&json!({ "operation": "start" }), None) + .await + .result + ); + assert!( + !tool + .validate_input(&json!({ "operation": "stop" }), None) + .await + .result + ); + // Discovery needs nothing at all. + assert!( + tool.validate_input(&json!({ "operation": "targets" }), None) + .await + .result + ); + } + + #[test] + fn endpoints_parse_the_shapes_an_agent_would_type() { + let parsed = parse_endpoint("root@10.0.0.5:2222").expect("user, host, and port"); + assert_eq!(parsed.user.as_deref(), Some("root")); + assert_eq!(parsed.host, "10.0.0.5"); + assert_eq!(parsed.port, Some(2222)); + + let alias = parse_endpoint("myserver").expect("a bare alias is a valid target"); + assert_eq!(alias.host, "myserver"); + assert_eq!(alias.user, None); + assert_eq!(alias.port, None); + + let ipv6 = parse_endpoint("[2001:db8::1]").expect("an IPv6 literal"); + assert_eq!(ipv6.host, "2001:db8::1"); + assert_eq!( + ipv6.port, None, + "the address's own colons are not a port number" + ); + + assert!(parse_endpoint("has space").is_none()); + assert!(parse_endpoint("host:not-a-port").is_none()); + } +} diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs index 9e799b11cb..b99b2e0237 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs @@ -82,6 +82,8 @@ impl StaticToolProviderFactory for ProductConcreteToolFactory { "SessionHistory" => Some(Arc::new(SessionHistoryTool::new())), #[cfg(feature = "tools-agent-control")] "Cron" => Some(Arc::new(CronTool::new())), + #[cfg(feature = "tools-agent-control")] + "PortForward" => Some(Arc::new(PortForwardTool::new())), #[cfg(feature = "tools-browser-web")] "WebSearch" => Some(Arc::new(WebSearchTool::new())), #[cfg(feature = "tools-browser-web")] diff --git a/src/crates/assembly/core/src/service/remote_ssh/mod.rs b/src/crates/assembly/core/src/service/remote_ssh/mod.rs index 13f4d7f365..8871b68988 100644 --- a/src/crates/assembly/core/src/service/remote_ssh/mod.rs +++ b/src/crates/assembly/core/src/service/remote_ssh/mod.rs @@ -20,9 +20,10 @@ pub use bitfun_services_integrations::remote_ssh::{ pub use bitfun_services_integrations::remote_ssh::{dispatch_ssh, relay_deploy}; #[cfg(not(feature = "ssh-remote"))] pub use bitfun_services_integrations::remote_ssh::{ - get_global_remote_exec_process_manager, KnownHostEntry, PTYSession, PortForward, - PortForwardDirection, PortForwardManager, RemoteExecCommandRequest, RemoteExecCommandResponse, - RemoteExecControlAction, RemoteExecControlOrigin, RemoteExecControlRequest, RemoteExecError, + get_global_remote_exec_process_manager, global_port_forward_manager, + list_remote_listening_ports, KnownHostEntry, PTYSession, PortForwardManager, + RemoteExecCommandRequest, RemoteExecCommandResponse, RemoteExecControlAction, + RemoteExecControlOrigin, RemoteExecControlRequest, RemoteExecError, RemoteExecProcessLifecycleEvent, RemoteExecProcessLifecycleStatus, RemoteExecProcessManager, RemoteExecResult, RemoteExecSessionCompletion, RemoteExecSessionCompletionSource, RemoteExecSessionCompletionStatus, RemoteFileService, RemoteSendStdinRequest, @@ -39,8 +40,8 @@ pub use bitfun_services_integrations::remote_ssh::{ }; #[cfg(feature = "ssh-remote")] pub use bitfun_services_integrations::remote_ssh::{ - KnownHostEntry, PTYSession, PortForward, PortForwardDirection, PortForwardManager, - SSHConnectionManager, + global_port_forward_manager, list_remote_listening_ports, KnownHostEntry, PTYSession, + PortForwardManager, SSHConnectionManager, }; #[cfg(feature = "ssh-remote")] pub use remote_fs::RemoteFileService; diff --git a/src/crates/execution/tool-provider-groups/src/lib.rs b/src/crates/execution/tool-provider-groups/src/lib.rs index 60558dcf6c..dc388eab33 100644 --- a/src/crates/execution/tool-provider-groups/src/lib.rs +++ b/src/crates/execution/tool-provider-groups/src/lib.rs @@ -105,7 +105,9 @@ pub fn tool_feature_group(tool_name: &str) -> Option { | "AgentDelete" | "AgentWait" | "LaunchReviewAgent" | "Skill" | "AskUserQuestion" | "TodoWrite" | "get_goal" | "create_goal" | "update_goal" | "CreatePlan" | "submit_code_review" | "GetToolSpec" | "CallDeferredTool" | "SessionControl" - | "SessionMessage" | "SessionHistory" | "Cron" => Some(ToolPackFeatureGroup::AgentControl), + | "SessionMessage" | "SessionHistory" | "Cron" | "PortForward" => { + Some(ToolPackFeatureGroup::AgentControl) + } _ => None, } } @@ -214,7 +216,13 @@ const PRODUCT_TOOL_PROVIDER_GROUP_PLAN: &[ToolProviderGroupPlan] = &[ ToolProviderGroupPlan { provider_id: "core.session", feature_groups: CORE_SESSION_FEATURE_GROUPS, - tool_names: &["SessionControl", "SessionMessage", "SessionHistory", "Cron"], + tool_names: &[ + "SessionControl", + "SessionMessage", + "SessionHistory", + "Cron", + "PortForward", + ], }, ToolProviderGroupPlan { provider_id: "core.integration", @@ -506,6 +514,7 @@ mod tests { "SessionMessage", "SessionHistory", "Cron", + "PortForward", "WebSearch", "WebFetch", "ListMCPResources", diff --git a/src/crates/services/services-integrations/src/remote_ssh/disabled.rs b/src/crates/services/services-integrations/src/remote_ssh/disabled.rs index 3cc3a8d935..20360f8823 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/disabled.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/disabled.rs @@ -5,9 +5,9 @@ //! stay behind `ssh-remote` and return explicit unsupported errors here. use crate::remote_ssh::types::{ - RemoteDirEntry, RemoteFileEntry, RemoteTreeNode, SSHCommandOptions, SSHCommandResult, - SSHConfigEntry, SSHConfigLookupResult, SSHConnectionConfig, SSHConnectionResult, - SavedConnection, ServerInfo, + PortForward, PortForwardRequest, RemoteDirEntry, RemoteFileEntry, RemoteListeningPort, + RemoteTreeNode, SSHCommandOptions, SSHCommandResult, SSHConfigEntry, SSHConfigLookupResult, + SSHConnectionConfig, SSHConnectionResult, SavedConnection, ServerInfo, }; use std::path::PathBuf; use std::sync::{Arc, OnceLock}; @@ -595,22 +595,6 @@ impl PTYSession { } } -#[derive(Debug, Clone)] -pub struct PortForward { - pub id: String, - pub local_port: u16, - pub remote_host: String, - pub remote_port: u16, - pub direction: PortForwardDirection, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PortForwardDirection { - Local, - Remote, - Dynamic, -} - #[derive(Clone, Default)] pub struct PortForwardManager; @@ -627,11 +611,8 @@ impl PortForwardManager { pub async fn start_local_forward( &self, - _connection_id: &str, - _local_port: u16, - _remote_host: &str, - _remote_port: u16, - ) -> anyhow::Result { + _request: &PortForwardRequest, + ) -> anyhow::Result { Err(unsupported()) } @@ -649,17 +630,45 @@ impl PortForwardManager { Err(unsupported()) } + pub async fn stop_for_connection(&self, _connection_id: &str) -> usize { + 0 + } + pub async fn stop_all(&self) {} pub async fn list_forwards(&self) -> Vec { Vec::new() } + pub async fn list_forwards_for_connection(&self, _connection_id: &str) -> Vec { + Vec::new() + } + + pub async fn find_forward( + &self, + _connection_id: &str, + _remote_host: &str, + _remote_port: u16, + ) -> Option { + None + } + pub async fn is_port_forwarded(&self, _port: u16) -> bool { false } } +pub fn global_port_forward_manager() -> PortForwardManager { + PortForwardManager +} + +pub async fn list_remote_listening_ports( + _manager: &SSHConnectionManager, + _connection_id: &str, +) -> anyhow::Result> { + Err(unsupported()) +} + #[derive(Debug, Clone)] pub struct RemoteFileService; diff --git a/src/crates/services/services-integrations/src/remote_ssh/manager.rs b/src/crates/services/services-integrations/src/remote_ssh/manager.rs index cec6b7a9d3..14dc2f13f9 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/manager.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/manager.rs @@ -4035,6 +4035,61 @@ impl SSHConnectionManager { Ok(channel) } + /// Connect a saved connection if it is not already live, and return once + /// the session is usable. + /// + /// This is the entry point for callers that hold a connection id but do not + /// know whether the user ever opened it in this run. It reuses the same + /// saved-profile load, reconnect serialization, and config-drift handling + /// as every other operation. + pub async fn ensure_connected(&self, connection_id: &str) -> anyhow::Result<()> { + self.ensure_alive_or_reconnect(connection_id).await + } + + /// Open a `direct-tcpip` channel to `host:port` as reached from the remote + /// end, which is the transport local port forwarding runs on. + /// + /// The handle is re-read from the connection map on every call rather than + /// cached by the caller: reconnect installs a *new* handle, and a forward + /// holding the old one would keep splicing traffic into a dead session. + pub async fn open_direct_tcpip( + &self, + connection_id: &str, + host: &str, + port: u16, + originator_host: &str, + originator_port: u16, + ) -> anyhow::Result> { + self.ensure_alive_or_reconnect(connection_id).await?; + let (handle, config) = { + let guard = self.connections.read().await; + let connection = guard + .get(connection_id) + .ok_or_else(|| anyhow!("Connection {} not found", connection_id))?; + ( + connection.handle.clone(), + connection.effective_config.clone(), + ) + }; + if config.uses_local_docker() { + anyhow::bail!("A local Docker workspace has no SSH transport to forward over"); + } + let handle = + handle.ok_or_else(|| anyhow!("SSH handle is unavailable for {}", connection_id))?; + + handle + .channel_open_direct_tcpip(host, port as u32, originator_host, originator_port as u32) + .await + .map_err(|e| { + anyhow!( + "Failed to reach {}:{} from the remote host: {}", + host, + port, + e + ) + }) + } + /// Open a transport-neutral, long-lived stdio process in the effective /// workspace target. /// @@ -5543,183 +5598,6 @@ impl PTYSession { } } -// ============================================================================ -// Port Forwarding -// ============================================================================ - -/// Port forwarding entry -#[derive(Debug, Clone)] -pub struct PortForward { - pub id: String, - pub local_port: u16, - pub remote_host: String, - pub remote_port: u16, - pub direction: PortForwardDirection, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PortForwardDirection { - Local, // -L: forward local port to remote - Remote, // -R: forward remote port to local - Dynamic, // -D: dynamic SOCKS proxy -} - -/// Port forwarding manager -pub struct PortForwardManager { - forwards: Arc>>, - ssh_manager: Arc>>, -} - -impl PortForwardManager { - pub fn new() -> Self { - Self { - forwards: Arc::new(tokio::sync::RwLock::new(HashMap::new())), - ssh_manager: Arc::new(tokio::sync::RwLock::new(None)), - } - } - - pub fn with_ssh_manager(ssh_manager: SSHConnectionManager) -> Self { - Self { - forwards: Arc::new(tokio::sync::RwLock::new(HashMap::new())), - ssh_manager: Arc::new(tokio::sync::RwLock::new(Some(ssh_manager))), - } - } - - pub async fn set_ssh_manager(&self, manager: SSHConnectionManager) { - let mut guard = self.ssh_manager.write().await; - *guard = Some(manager); - } - - /// Start local port forwarding (-L) - /// - /// TODO: Full implementation requires: - /// - TCP listener to accept local connections - /// - SSH channel for each forwarded connection - /// - Proper cleanup when stopping the forward - /// - /// Currently this is a placeholder that only tracks the forward configuration. - pub async fn start_local_forward( - &self, - _connection_id: &str, - local_port: u16, - remote_host: String, - remote_port: u16, - ) -> anyhow::Result { - let id = uuid::Uuid::new_v4().to_string(); - - let forward = PortForward { - id: id.clone(), - local_port, - remote_host: remote_host.clone(), - remote_port, - direction: PortForwardDirection::Local, - }; - - // Store forward entry - let mut guard = self.forwards.write().await; - guard.insert(id.clone(), forward); - - log::info!( - "[TODO] Local port forward registered: localhost:{} -> {}:{}", - local_port, - remote_host, - remote_port - ); - log::warn!("Port forwarding is not fully implemented - connections will not be forwarded"); - - Ok(id) - } - - /// Start remote port forwarding (-R) - /// - /// TODO: Full implementation requires SSH reverse port forwarding channel. - /// This is more complex as it needs to bind to a remote port. - pub async fn start_remote_forward( - &self, - _connection_id: &str, - remote_port: u16, - local_host: String, - local_port: u16, - ) -> anyhow::Result { - let id = uuid::Uuid::new_v4().to_string(); - - let forward = PortForward { - id: id.clone(), - local_port: remote_port, - remote_host: local_host.clone(), - remote_port: local_port, - direction: PortForwardDirection::Remote, - }; - - // Remote port forwarding requires SSH channel forwarding - // This is a placeholder - full implementation would need: - // 1. Open a "reverse" channel on SSH connection - // 2. Bind to remote port - // 3. Forward connections back through the channel - - let mut guard = self.forwards.write().await; - guard.insert(id.clone(), forward); - - log::info!( - "Started remote port forward (placeholder): *:{} -> {}:{}", - remote_port, - local_host, - local_port - ); - - // TODO: Implement actual SSH reverse port forwarding - log::warn!("Remote port forwarding is not fully implemented - data will not be forwarded"); - - Ok(id) - } - - /// Stop a port forward - pub async fn stop_forward(&self, forward_id: &str) -> anyhow::Result<()> { - let mut guard = self.forwards.write().await; - if let Some(forward) = guard.remove(forward_id) { - log::info!( - "Stopped port forward: {} ({}:{} -> {}:{})", - forward.id, - match forward.direction { - PortForwardDirection::Local => "local", - PortForwardDirection::Remote => "remote", - PortForwardDirection::Dynamic => "dynamic", - }, - forward.local_port, - forward.remote_host, - forward.remote_port - ); - } - Ok(()) - } - - /// Stop all port forwards - pub async fn stop_all(&self) { - let mut guard = self.forwards.write().await; - let count = guard.len(); - guard.drain(); - log::info!("All {} port forwards stopped", count); - } - - /// List all active forwards - pub async fn list_forwards(&self) -> Vec { - let guard = self.forwards.read().await; - guard.values().cloned().collect() - } - - /// Check if a port is already forwarded - pub async fn is_port_forwarded(&self, port: u16) -> bool { - let guard = self.forwards.read().await; - guard.values().any(|f| f.local_port == port) - } -} - -impl Default for PortForwardManager { - fn default() -> Self { - Self::new() - } -} - fn sftp_mkdir_all_prefixes(path: &str) -> Vec { let is_absolute = path.starts_with('/'); let mut current = String::new(); diff --git a/src/crates/services/services-integrations/src/remote_ssh/mod.rs b/src/crates/services/services-integrations/src/remote_ssh/mod.rs index abe62202b4..959b3420ec 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/mod.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/mod.rs @@ -24,6 +24,8 @@ pub mod manager; #[cfg(feature = "remote-ssh-concrete")] mod password_vault; #[cfg(feature = "remote-ssh-concrete")] +mod port_forward; +#[cfg(feature = "remote-ssh-concrete")] pub mod relay_deploy; #[cfg(feature = "remote-ssh-concrete")] mod release_verify; @@ -50,9 +52,10 @@ pub use workspace_services::{remote_workspace_services, RemoteWorkspaceFs, Remot #[cfg(not(feature = "remote-ssh-concrete"))] pub use disabled::{ - dispatch_ssh, get_global_remote_exec_process_manager, KnownHostEntry, PTYSession, PortForward, - PortForwardDirection, PortForwardManager, RemoteExecCommandRequest, RemoteExecCommandResponse, - RemoteExecControlAction, RemoteExecControlOrigin, RemoteExecControlRequest, RemoteExecError, + dispatch_ssh, get_global_remote_exec_process_manager, global_port_forward_manager, + list_remote_listening_ports, KnownHostEntry, PTYSession, PortForwardManager, + RemoteExecCommandRequest, RemoteExecCommandResponse, RemoteExecControlAction, + RemoteExecControlOrigin, RemoteExecControlRequest, RemoteExecError, RemoteExecProcessLifecycleEvent, RemoteExecProcessLifecycleStatus, RemoteExecProcessManager, RemoteExecResult, RemoteExecSessionCompletion, RemoteExecSessionCompletionSource, RemoteExecSessionCompletionStatus, RemoteFileService, RemoteSendStdinRequest, @@ -60,9 +63,10 @@ pub use disabled::{ SessionStatus, }; #[cfg(feature = "remote-ssh-concrete")] -pub use manager::{ - KnownHostEntry, PTYSession, PortForward, PortForwardDirection, PortForwardManager, - SSHConnectionManager, +pub use manager::{KnownHostEntry, PTYSession, SSHConnectionManager}; +#[cfg(feature = "remote-ssh-concrete")] +pub use port_forward::{ + global_port_forward_manager, list_remote_listening_ports, PortForwardManager, }; #[cfg(feature = "remote-ssh-concrete")] pub use remote_exec::{ diff --git a/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs b/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs new file mode 100644 index 0000000000..e820c60e28 --- /dev/null +++ b/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs @@ -0,0 +1,1099 @@ +//! Local (`ssh -L`) port forwarding over an established SSH session. +//! +//! A forward is a listener on this machine whose accepted connections are +//! spliced onto a `direct-tcpip` channel, so the remote sshd dials the service +//! and the two streams are copied in both directions. +//! +//! Two properties drive the design: +//! +//! - **The remote endpoint is the identity, the local port is an allocation.** +//! Users care that "the dev server on the remote's port 3000 is reachable +//! from here"; which local port carries it is a detail. Binding therefore +//! falls back to an arbitrary free port instead of failing, and reports what +//! was asked for so the UI can explain the move. +//! - **A forward must never resurrect a session the user closed.** Channels are +//! opened through [`SSHConnectionManager`] on every connection rather than +//! against a cached handle, which is what keeps a forward working across an +//! automatic reconnect; the liveness check before each open is what stops +//! that same path from silently dialing back out after an explicit +//! disconnect. + +use anyhow::{anyhow, Context}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::{Arc, OnceLock}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::time::Duration; +use tokio_util::sync::CancellationToken; + +use crate::remote_ssh::manager::SSHConnectionManager; +use crate::remote_ssh::types::{ + PortForward, PortForwardDirection, PortForwardRequest, RemoteListeningPort, SSHCommandOptions, +}; + +/// Upper bound on connections a single forward carries at once. +/// +/// A browser opens a handful of sockets per origin, so this sits far above +/// normal use. It exists so one runaway client cannot open SSH channels without +/// limit on a session that is also carrying the editor's own traffic. +const MAX_CONCURRENT_CONNECTIONS: usize = 256; + +/// Originating address reported to the remote when opening a channel. The +/// remote only uses it for logging. +const ORIGINATOR_HOST: &str = "127.0.0.1"; + +/// Pause after a failed `accept` so a persistent error (descriptor exhaustion, +/// most likely) cannot spin the accept loop hot. +const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(50); + +/// How long the remote gets to report its listening sockets. +const LISTENING_PORT_PROBE_TIMEOUT_MS: u64 = 10_000; + +/// Ports never worth offering as forwarding candidates. Port 22 is the +/// session's own transport: it is listening on every host reachable this way, +/// and forwarding it would only ever produce a second path to the SSH server +/// already carrying the forward. +const UNINTERESTING_LISTENING_PORTS: &[u16] = &[22]; + +/// Counters and health for one running forward. +/// +/// Snapshots read these instead of the accept loop pushing updates, so the UI +/// can poll at whatever rate it likes without the data path paying for it. +struct ForwardRuntime { + active_connections: AtomicU32, + total_connections: AtomicU64, + last_error: std::sync::Mutex>, +} + +impl ForwardRuntime { + fn new() -> Self { + Self { + active_connections: AtomicU32::new(0), + total_connections: AtomicU64::new(0), + last_error: std::sync::Mutex::new(None), + } + } + + fn connection_opened(&self) { + self.active_connections.fetch_add(1, Ordering::SeqCst); + self.total_connections.fetch_add(1, Ordering::SeqCst); + } + + fn connection_closed(&self) { + // Saturate rather than wrap. The counter should never reach zero with a + // connection still open, but underflowing it would report four billion + // active connections in the UI and there is no failure worth that. + let _ = + self.active_connections + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |active| { + Some(active.saturating_sub(1)) + }); + } + + fn record_error(&self, message: String) { + if let Ok(mut guard) = self.last_error.lock() { + *guard = Some(message); + } + } + + /// Clear the health signal after a connection completes normally, so a + /// forward started before its remote service was up stops looking broken + /// once the service arrives. + fn clear_error(&self) { + if let Ok(mut guard) = self.last_error.lock() { + *guard = None; + } + } + + fn last_error(&self) -> Option { + self.last_error + .lock() + .ok() + .and_then(|guard| guard.as_ref().cloned()) + } +} + +/// A registered forward: its descriptor, live counters, and shutdown handle. +struct ForwardEntry { + /// Descriptor fields that never change while the forward runs. Counters and + /// `last_error` on this value are stale by construction and are refilled by + /// [`ForwardEntry::snapshot`]. + descriptor: PortForward, + runtime: Arc, + cancel: CancellationToken, +} + +impl ForwardEntry { + fn snapshot(&self) -> PortForward { + let mut forward = self.descriptor.clone(); + forward.active_connections = self.runtime.active_connections.load(Ordering::SeqCst); + forward.total_connections = self.runtime.total_connections.load(Ordering::SeqCst); + forward.last_error = self.runtime.last_error(); + forward + } +} + +/// Whether a failed bind should be retried on an arbitrary port. +/// +/// `AddrInUse` is the everyday case. `PermissionDenied` covers privileged +/// ports, where telling the user to run the editor as root would be a far worse +/// answer than moving the mapping. +fn bind_error_can_fall_back(error: &std::io::Error) -> bool { + matches!( + error.kind(), + std::io::ErrorKind::AddrInUse | std::io::ErrorKind::PermissionDenied + ) +} + +/// Whether a copy error is just the peer hanging up. +/// +/// Closing a browser tab mid-request produces these, and surfacing them would +/// leave a healthy forward permanently flagged as failing. +fn stream_error_is_benign(error: &std::io::Error) -> bool { + matches!( + error.kind(), + std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::BrokenPipe + | std::io::ErrorKind::NotConnected + | std::io::ErrorKind::UnexpectedEof + ) +} + +/// Bind the listener for a local forward, falling back to an arbitrary free +/// port when the requested one cannot be taken. +/// +/// Returns the listener and, when the requested port was unavailable, the port +/// that was asked for. Callers need that to tell the user the mapping moved +/// rather than quietly handing back a different address. +async fn bind_forward_listener( + host: &str, + preferred: Option, +) -> anyhow::Result<(TcpListener, Option)> { + let Some(port) = preferred else { + let listener = TcpListener::bind((host, 0)) + .await + .with_context(|| format!("Failed to bind {host} for port forwarding"))?; + return Ok((listener, None)); + }; + + match TcpListener::bind((host, port)).await { + Ok(listener) => Ok((listener, None)), + Err(error) if bind_error_can_fall_back(&error) => { + let listener = TcpListener::bind((host, 0)) + .await + .with_context(|| format!("Failed to bind a fallback local port on {host}"))?; + Ok((listener, Some(port))) + } + Err(error) => Err(anyhow!("Failed to bind {host}:{port}: {error}")), + } +} + +/// Port forwarding manager. +/// +/// Owns local (`-L`) forwards. Reverse and dynamic forwards keep their entry +/// point so callers get an explicit error rather than a silent no-op. +#[derive(Clone)] +pub struct PortForwardManager { + forwards: Arc>>, + ssh_manager: Arc>>, +} + +impl PortForwardManager { + pub fn new() -> Self { + Self { + forwards: Arc::new(tokio::sync::RwLock::new(HashMap::new())), + ssh_manager: Arc::new(tokio::sync::RwLock::new(None)), + } + } + + pub fn with_ssh_manager(ssh_manager: SSHConnectionManager) -> Self { + Self { + forwards: Arc::new(tokio::sync::RwLock::new(HashMap::new())), + ssh_manager: Arc::new(tokio::sync::RwLock::new(Some(ssh_manager))), + } + } + + pub async fn set_ssh_manager(&self, manager: SSHConnectionManager) { + let mut guard = self.ssh_manager.write().await; + *guard = Some(manager); + } + + async fn ssh_manager(&self) -> anyhow::Result { + self.ssh_manager + .read() + .await + .clone() + .ok_or_else(|| anyhow!("SSH manager is not initialized")) + } + + /// Start a local (`-L`) forward and return the mapping that was actually + /// established, which is not always the one that was requested. + pub async fn start_local_forward( + &self, + request: &PortForwardRequest, + ) -> anyhow::Result { + let connection_id = request.connection_id.trim(); + if connection_id.is_empty() { + anyhow::bail!("A port forward needs a connection id"); + } + if request.remote_port == 0 { + anyhow::bail!("A port forward needs a remote port"); + } + + let manager = self.ssh_manager().await?; + // Starting a forward is an explicit request, so a saved connection the + // user has not opened yet is connected rather than refused. The + // liveness gate that matters is the per-connection one in + // `carry_connection`, which is what stops a *stale* forward from + // dialing back out on its own. + manager + .ensure_connected(connection_id) + .await + .with_context(|| format!("SSH connection '{connection_id}' is not usable"))?; + // Reject the one target that can never work, here rather than on the + // first request: a local Docker workspace is reached by `docker exec` + // and has no SSH session to carry a channel. Discovering that after + // opening a browser would be a worse way to learn it. + if manager.is_local_container_connection(connection_id).await { + anyhow::bail!( + "A local Docker workspace has no SSH transport to forward over. \ + Publish the container port with `docker run -p` instead." + ); + } + + let remote_host = request.effective_remote_host().to_string(); + let local_host = request.effective_local_host(); + + // Hold the registry across the duplicate check, the bind, and the + // insert. Checking under a read lock and inserting later would let two + // concurrent requests for the same endpoint both pass the check and + // both bind, leaving the user with two local addresses that mean the + // same thing. Binding is a syscall, so serializing starts costs + // nothing worth measuring. + let mut guard = self.forwards.write().await; + + if let Some(existing) = guard.values().find(|entry| { + entry.descriptor.connection_id == connection_id + && entry.descriptor.remote_host == remote_host + && entry.descriptor.remote_port == request.remote_port + }) { + anyhow::bail!( + "{}:{} is already forwarded to {}", + remote_host, + request.remote_port, + existing.descriptor.local_address() + ); + } + + let (listener, requested_local_port) = + bind_forward_listener(local_host, request.preferred_local_port()).await?; + let local_port = listener + .local_addr() + .context("Failed to read the bound local address")? + .port(); + + let descriptor = PortForward { + id: uuid::Uuid::new_v4().to_string(), + connection_id: connection_id.to_string(), + direction: PortForwardDirection::Local, + label: request.normalized_label(), + local_host: local_host.to_string(), + local_port, + requested_local_port, + remote_host, + remote_port: request.remote_port, + active_connections: 0, + total_connections: 0, + last_error: None, + }; + + let runtime = Arc::new(ForwardRuntime::new()); + let cancel = CancellationToken::new(); + + log::info!( + "Port forward started: {} -> {}:{} (connection {})", + descriptor.local_address(), + descriptor.remote_host, + descriptor.remote_port, + descriptor.connection_id + ); + if let Some(requested) = requested_local_port { + log::info!( + "Local port {} was unavailable; forward {} bound {} instead", + requested, + descriptor.id, + local_port + ); + } + + Self::spawn_accept_loop( + listener, + manager, + descriptor.clone(), + runtime.clone(), + cancel.clone(), + ); + + guard.insert( + descriptor.id.clone(), + ForwardEntry { + descriptor: descriptor.clone(), + runtime, + cancel, + }, + ); + + Ok(descriptor) + } + + /// Reverse (`-R`) forwarding entry point. + /// + /// Not implemented: it needs `tcpip-forward` global requests and a handler + /// for server-initiated `forwarded-tcpip` channels, neither of which the + /// session handler carries today. It fails loudly rather than registering a + /// forward that moves no data. + pub async fn start_remote_forward( + &self, + _connection_id: &str, + _remote_port: u16, + _local_host: &str, + _local_port: u16, + ) -> anyhow::Result { + Err(anyhow!("Reverse port forwarding is not supported yet")) + } + + fn spawn_accept_loop( + listener: TcpListener, + manager: SSHConnectionManager, + descriptor: PortForward, + runtime: Arc, + cancel: CancellationToken, + ) { + tokio::spawn(async move { + let permits = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_CONNECTIONS)); + loop { + let accepted = tokio::select! { + _ = cancel.cancelled() => break, + accepted = listener.accept() => accepted, + }; + + let (stream, peer) = match accepted { + Ok(accepted) => accepted, + Err(error) => { + // One client failing its handshake must not take the + // whole forward down; report it and keep listening. + runtime + .record_error(format!("Failed to accept a local connection: {error}")); + tokio::time::sleep(ACCEPT_ERROR_BACKOFF).await; + continue; + } + }; + + // Wait for a slot under the same cancellation as the accept + // above. Awaiting the permit bare would keep a stopped forward + // alive until an in-flight connection released one, and then + // open a channel for a client nobody is going to serve. + let permit = tokio::select! { + _ = cancel.cancelled() => break, + permit = permits.clone().acquire_owned() => match permit { + Ok(permit) => permit, + Err(_) => break, + }, + }; + + let manager = manager.clone(); + let descriptor = descriptor.clone(); + let runtime = runtime.clone(); + let cancel = cancel.clone(); + tokio::spawn(async move { + Self::carry_connection( + manager, + descriptor, + runtime, + cancel, + stream, + peer.port(), + ) + .await; + drop(permit); + }); + } + + log::info!( + "Port forward stopped: {} -> {}:{}", + descriptor.local_address(), + descriptor.remote_host, + descriptor.remote_port + ); + }); + } + + async fn carry_connection( + manager: SSHConnectionManager, + descriptor: PortForward, + runtime: Arc, + cancel: CancellationToken, + mut local: TcpStream, + peer_port: u16, + ) { + // Gate on liveness first. `open_direct_tcpip` reconnects on demand, + // which is exactly what keeps a forward alive across a dropped session + // — and exactly what would let a forward the caller forgot to stop dial + // back out to a host the user explicitly disconnected from. + if !manager.is_connected(&descriptor.connection_id).await { + runtime.record_error("SSH connection is not active".to_string()); + return; + } + + let channel = match manager + .open_direct_tcpip( + &descriptor.connection_id, + &descriptor.remote_host, + descriptor.remote_port, + ORIGINATOR_HOST, + peer_port, + ) + .await + { + Ok(channel) => channel, + Err(error) => { + runtime.record_error(format!("{error:#}")); + return; + } + }; + + runtime.connection_opened(); + let mut remote = channel.into_stream(); + let outcome = tokio::select! { + _ = cancel.cancelled() => None, + copied = tokio::io::copy_bidirectional(&mut local, &mut remote) => Some(copied), + }; + match outcome { + Some(Err(error)) if !stream_error_is_benign(&error) => { + runtime.record_error(format!("Forwarded connection failed: {error}")); + } + Some(_) => runtime.clear_error(), + None => {} + } + runtime.connection_closed(); + } + + /// Stop one forward. Idempotent: stopping an unknown id succeeds so a UI + /// that raced a teardown does not have to special-case the reply. + pub async fn stop_forward(&self, forward_id: &str) -> anyhow::Result<()> { + let removed = { + let mut guard = self.forwards.write().await; + guard.remove(forward_id) + }; + if let Some(entry) = removed { + entry.cancel.cancel(); + log::info!( + "Stopped port forward {}: {} -> {}:{}", + entry.descriptor.id, + entry.descriptor.local_address(), + entry.descriptor.remote_host, + entry.descriptor.remote_port + ); + } + Ok(()) + } + + /// Stop every forward carried by one connection. + /// + /// Called when a connection is torn down, so the listeners go away with the + /// session that gave them meaning. + pub async fn stop_for_connection(&self, connection_id: &str) -> usize { + let removed: Vec = { + let mut guard = self.forwards.write().await; + let ids: Vec = guard + .values() + .filter(|entry| entry.descriptor.connection_id == connection_id) + .map(|entry| entry.descriptor.id.clone()) + .collect(); + ids.iter().filter_map(|id| guard.remove(id)).collect() + }; + for entry in &removed { + entry.cancel.cancel(); + } + if !removed.is_empty() { + log::info!( + "Stopped {} port forward(s) for connection {}", + removed.len(), + connection_id + ); + } + removed.len() + } + + /// Stop all port forwards. + pub async fn stop_all(&self) { + let removed: Vec = { + let mut guard = self.forwards.write().await; + guard.drain().map(|(_, entry)| entry).collect() + }; + for entry in &removed { + entry.cancel.cancel(); + } + if !removed.is_empty() { + log::info!("Stopped all {} port forward(s)", removed.len()); + } + } + + /// List every forward, ordered so the UI does not reshuffle between polls. + pub async fn list_forwards(&self) -> Vec { + let guard = self.forwards.read().await; + let mut forwards: Vec = guard.values().map(ForwardEntry::snapshot).collect(); + forwards.sort_by(|left, right| { + left.connection_id + .cmp(&right.connection_id) + .then(left.remote_port.cmp(&right.remote_port)) + .then(left.remote_host.cmp(&right.remote_host)) + }); + forwards + } + + /// List the forwards carried by one connection. + pub async fn list_forwards_for_connection(&self, connection_id: &str) -> Vec { + self.list_forwards() + .await + .into_iter() + .filter(|forward| forward.connection_id == connection_id) + .collect() + } + + /// Look up the forward that already covers a remote endpoint. + pub async fn find_forward( + &self, + connection_id: &str, + remote_host: &str, + remote_port: u16, + ) -> Option { + let guard = self.forwards.read().await; + guard + .values() + .find(|entry| { + entry.descriptor.connection_id == connection_id + && entry.descriptor.remote_host == remote_host + && entry.descriptor.remote_port == remote_port + }) + .map(ForwardEntry::snapshot) + } + + /// Whether some forward is bound to a local port on this machine. + pub async fn is_port_forwarded(&self, port: u16) -> bool { + let guard = self.forwards.read().await; + guard + .values() + .any(|entry| entry.descriptor.local_port == port) + } +} + +static GLOBAL_PORT_FORWARD_MANAGER: OnceLock = OnceLock::new(); + +/// The process-wide forward registry. +/// +/// Both the desktop UI and the Agent tool operate on the same set of forwards: +/// a mapping the user created by hand and one an Agent created are the same +/// object, and either surface must be able to list and stop the other's work. +/// Handing each caller its own manager would split that in half and leak +/// listeners neither side could see. +pub fn global_port_forward_manager() -> PortForwardManager { + GLOBAL_PORT_FORWARD_MANAGER + .get_or_init(PortForwardManager::new) + .clone() +} + +impl Default for PortForwardManager { + fn default() -> Self { + Self::new() + } +} + +// ============================================================================ +// Remote listening port discovery +// ============================================================================ + +/// Which tool produced a listening-socket listing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ListingFormat { + /// `ss -H -l -t -n -p`, and `netstat -l -t -n -p`, which agree on the + /// column that holds the local address. + ColumnarSocketStat, + /// `lsof -nP -iTCP -sTCP:LISTEN`. + Lsof, +} + +/// Probe script run on the remote. +/// +/// `ss` first because it is the modern Linux tool, then `lsof` because macOS +/// remotes have it and their `netstat` spells `-p` as "protocol", then +/// `netstat` for older Linux hosts. The marker line tells the parser which +/// dialect came back instead of making it guess from the shape. +const LISTENING_PORT_PROBE_SCRIPT: &str = concat!( + "if command -v ss >/dev/null 2>&1; then ", + "echo __BITFUN_SS__; ss -H -l -t -n -p 2>/dev/null; ", + "elif command -v lsof >/dev/null 2>&1; then ", + "echo __BITFUN_LSOF__; lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null; ", + "elif command -v netstat >/dev/null 2>&1; then ", + "echo __BITFUN_NETSTAT__; netstat -l -t -n -p 2>/dev/null; ", + "else echo __BITFUN_NONE__; fi" +); + +/// Split a `host:port` token, tolerating the IPv6 and wildcard spellings the +/// various tools emit (`[::]:80`, `*:3000`, `:::80`). +fn split_host_port(token: &str) -> Option<(String, u16)> { + let (host, port) = token.rsplit_once(':')?; + let port: u16 = port.trim().parse().ok()?; + let host = host.trim(); + let host = host + .strip_prefix('[') + .and_then(|rest| rest.strip_suffix(']')) + .unwrap_or(host); + let host = match host { + "" | "*" => "0.0.0.0", + other => other, + }; + Some((host.to_string(), port)) +} + +/// Pull the process name and pid out of the trailing column. +/// +/// `ss` writes `users:(("node",pid=999,fd=20))`; `netstat` writes `999/node`. +fn parse_socket_stat_process(line: &str) -> (Option, Option) { + if let Some(users) = line.split("users:((").nth(1) { + let name = users + .split('"') + .nth(1) + .map(str::to_string) + .filter(|name| !name.is_empty()); + let pid = users + .split("pid=") + .nth(1) + .and_then(|rest| { + rest.split(|c: char| !c.is_ascii_digit()) + .find(|part| !part.is_empty()) + }) + .and_then(|digits| digits.parse().ok()); + return (name, pid); + } + + for field in line.split_whitespace() { + if let Some((pid, name)) = field.split_once('/') { + if let Ok(pid) = pid.parse::() { + let name = name.trim(); + return ( + (!name.is_empty() && name != "-").then(|| name.to_string()), + Some(pid), + ); + } + } + } + (None, None) +} + +fn parse_columnar_socket_stat(output: &str) -> Vec { + let mut ports = Vec::new(); + for line in output.lines() { + let fields: Vec<&str> = line.split_whitespace().collect(); + // `ss -H` emits `LISTEN Recv-Q Send-Q Local Peer [users]`; `netstat` + // emits `Proto Recv-Q Send-Q Local Foreign State [pid/name]`. Both put + // the local address in the fourth column, and neither header row + // survives the state/protocol check below. + if fields.len() < 4 { + continue; + } + let is_listening_row = fields[0].eq_ignore_ascii_case("LISTEN") + || (fields[0].starts_with("tcp") + && fields + .iter() + .any(|field| field.eq_ignore_ascii_case("LISTEN"))); + if !is_listening_row { + continue; + } + let Some((bind_address, port)) = split_host_port(fields[3]) else { + continue; + }; + let (process, pid) = parse_socket_stat_process(line); + ports.push(RemoteListeningPort { + port, + bind_address, + process, + pid, + }); + } + ports +} + +fn parse_lsof(output: &str) -> Vec { + let mut ports = Vec::new(); + for line in output.lines() { + if !line.contains("(LISTEN)") { + continue; + } + let fields: Vec<&str> = line.split_whitespace().collect(); + // `COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME (LISTEN)`. + if fields.len() < 9 { + continue; + } + // The name column is the field immediately before `(LISTEN)`, which is + // more robust than indexing from the left: command names can contain + // spaces on some platforms. + let Some(name_index) = fields.iter().position(|field| *field == "(LISTEN)") else { + continue; + }; + let Some(address) = name_index + .checked_sub(1) + .and_then(|index| fields.get(index)) + else { + continue; + }; + // lsof writes `host:port` but also `*:port` and `[::1]:port`. + let Some((bind_address, port)) = split_host_port(address) else { + continue; + }; + ports.push(RemoteListeningPort { + port, + bind_address, + process: Some(fields[0].to_string()).filter(|name| !name.is_empty()), + pid: fields[1].parse().ok(), + }); + } + ports +} + +/// Collapse a listing into one row per port. +/// +/// A service bound to both address families shows up twice, and the port is the +/// only part the user acts on. Rows that name a process win, so the merged +/// entry keeps the most useful label. +fn dedupe_listening_ports(ports: Vec) -> Vec { + let mut merged: Vec = Vec::new(); + for port in ports { + if UNINTERESTING_LISTENING_PORTS.contains(&port.port) { + continue; + } + match merged.iter_mut().find(|kept| kept.port == port.port) { + Some(kept) => { + if kept.process.is_none() && port.process.is_some() { + kept.process = port.process; + kept.pid = port.pid; + kept.bind_address = port.bind_address; + } + } + None => merged.push(port), + } + } + merged.sort_by_key(|port| port.port); + merged +} + +fn parse_listening_ports(output: &str) -> anyhow::Result> { + let mut lines = output.lines(); + let format = loop { + let Some(line) = lines.next() else { + anyhow::bail!("The remote produced no listening-socket listing"); + }; + match line.trim() { + "__BITFUN_SS__" | "__BITFUN_NETSTAT__" => break ListingFormat::ColumnarSocketStat, + "__BITFUN_LSOF__" => break ListingFormat::Lsof, + "__BITFUN_NONE__" => anyhow::bail!( + "The remote host has none of ss, lsof, or netstat, so its listening ports \ + cannot be detected. Enter the port manually." + ), + // Shell profiles love to print banners; skip until the marker. + _ => continue, + } + }; + + let body: String = lines.collect::>().join("\n"); + let ports = match format { + ListingFormat::ColumnarSocketStat => parse_columnar_socket_stat(&body), + ListingFormat::Lsof => parse_lsof(&body), + }; + Ok(dedupe_listening_ports(ports)) +} + +/// List the TCP ports currently accepting connections on the remote host. +/// +/// This is the opt-in alternative to guessing a port number; nothing is +/// forwarded as a result of calling it. +pub async fn list_remote_listening_ports( + manager: &SSHConnectionManager, + connection_id: &str, +) -> anyhow::Result> { + let result = manager + .execute_command_with_options( + connection_id, + LISTENING_PORT_PROBE_SCRIPT, + SSHCommandOptions { + timeout_ms: Some(LISTENING_PORT_PROBE_TIMEOUT_MS), + cancellation_token: None, + }, + ) + .await + .context("Failed to probe the remote host for listening ports")?; + + // A non-zero status is expected: the probe tolerates a tool that partially + // fails, and only a listing with no marker at all is unusable. + parse_listening_ports(&result.stdout) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_ipv4_ipv6_and_wildcard_addresses() { + assert_eq!( + split_host_port("127.0.0.1:3000"), + Some(("127.0.0.1".to_string(), 3000)) + ); + // The IPv6 wildcard keeps its own spelling: it is reported to the user + // as the address the remote service is actually bound to, and claiming + // it is an IPv4 wildcard would be a lie. + assert_eq!(split_host_port("[::]:8080"), Some(("::".to_string(), 8080))); + assert_eq!(split_host_port(":::80"), Some(("::".to_string(), 80))); + // `*` has no address family attached, so loopback-or-anything is the + // only honest reading and `0.0.0.0` is how the other tools spell it. + assert_eq!( + split_host_port("*:5173"), + Some(("0.0.0.0".to_string(), 5173)) + ); + assert_eq!(split_host_port("no-port"), None); + } + + #[test] + fn parses_ss_listing() { + let output = "__BITFUN_SS__\n\ +LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:((\"node\",pid=999,fd=20))\n\ +LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:((\"nginx\",pid=1234,fd=6))\n\ +LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=700,fd=3))\n"; + let ports = parse_listening_ports(output).expect("listing should parse"); + assert_eq!(ports.len(), 2, "port 22 is filtered out: {ports:?}"); + assert_eq!(ports[0].port, 3000); + assert_eq!(ports[0].process.as_deref(), Some("node")); + assert_eq!(ports[0].pid, Some(999)); + assert_eq!(ports[1].port, 8080); + assert_eq!(ports[1].process.as_deref(), Some("nginx")); + } + + #[test] + fn parses_netstat_listing() { + let output = "__BITFUN_NETSTAT__\n\ +Active Internet connections (only servers)\n\ +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name\n\ +tcp 0 0 127.0.0.1:5173 0.0.0.0:* LISTEN 4242/vite\n\ +tcp6 0 0 :::9229 :::* LISTEN -\n"; + let ports = parse_listening_ports(output).expect("listing should parse"); + assert_eq!(ports.len(), 2); + assert_eq!(ports[0].port, 5173); + assert_eq!(ports[0].process.as_deref(), Some("vite")); + assert_eq!(ports[0].pid, Some(4242)); + assert_eq!(ports[1].port, 9229); + assert_eq!(ports[1].process, None); + } + + #[test] + fn parses_lsof_listing() { + let output = "__BITFUN_LSOF__\n\ +COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n\ +node 99999 bob 20u IPv4 0x1234567890abcdef 0t0 TCP 127.0.0.1:3000 (LISTEN)\n\ +Python 55555 bob 3u IPv6 0xfedcba0987654321 0t0 TCP *:8000 (LISTEN)\n"; + let ports = parse_listening_ports(output).expect("listing should parse"); + assert_eq!(ports.len(), 2); + assert_eq!(ports[0].port, 3000); + assert_eq!(ports[0].process.as_deref(), Some("node")); + assert_eq!(ports[0].pid, Some(99999)); + assert_eq!(ports[1].port, 8000); + assert_eq!(ports[1].bind_address, "0.0.0.0"); + } + + #[test] + fn skips_shell_banner_before_the_marker() { + let output = "Welcome to Ubuntu 24.04 LTS\n\ +Last login: Tue Aug 19 09:00:00 2026\n\ +__BITFUN_SS__\n\ +LISTEN 0 4096 127.0.0.1:4000 0.0.0.0:* users:((\"api\",pid=12,fd=7))\n"; + let ports = parse_listening_ports(output).expect("listing should parse"); + assert_eq!(ports.len(), 1); + assert_eq!(ports[0].port, 4000); + } + + #[test] + fn reports_a_remote_without_any_probe_tool() { + let error = parse_listening_ports("__BITFUN_NONE__\n") + .expect_err("a remote with no tooling should be an error"); + assert!( + error.to_string().contains("manually"), + "the message should tell the user what to do instead: {error}" + ); + } + + #[test] + fn merges_dual_stack_rows_and_prefers_the_named_process() { + let ports = dedupe_listening_ports(vec![ + RemoteListeningPort { + port: 3000, + bind_address: "0.0.0.0".to_string(), + process: None, + pid: None, + }, + RemoteListeningPort { + port: 3000, + bind_address: "::".to_string(), + process: Some("node".to_string()), + pid: Some(7), + }, + ]); + assert_eq!(ports.len(), 1); + assert_eq!(ports[0].process.as_deref(), Some("node")); + assert_eq!(ports[0].pid, Some(7)); + } + + #[tokio::test] + async fn falls_back_to_a_free_port_when_the_requested_one_is_taken() { + let occupied = TcpListener::bind(("127.0.0.1", 0)) + .await + .expect("the test needs a port to occupy"); + let taken = occupied.local_addr().expect("bound address").port(); + + let (listener, requested) = bind_forward_listener("127.0.0.1", Some(taken)) + .await + .expect("binding should fall back rather than fail"); + let bound = listener.local_addr().expect("bound address").port(); + + assert_eq!( + requested, + Some(taken), + "the requested port must be reported so the UI can explain the move" + ); + assert_ne!(bound, taken); + } + + #[tokio::test] + async fn keeps_the_requested_port_when_it_is_free() { + // Freeing a port and asking for it back is inherently racy: anything + // else on the machine — including the rest of this suite, which binds + // ephemeral ports in parallel — may claim the number in between. Retry + // so a stolen port cannot fail a test about the *un*-stolen path. + const ATTEMPTS: usize = 16; + for _ in 0..ATTEMPTS { + let probe = TcpListener::bind(("127.0.0.1", 0)) + .await + .expect("the test needs a free port number"); + let free = probe.local_addr().expect("bound address").port(); + drop(probe); + + let (listener, requested) = bind_forward_listener("127.0.0.1", Some(free)) + .await + .expect("binding must succeed whether or not the port was taken"); + + if requested.is_none() { + assert_eq!( + listener.local_addr().expect("bound address").port(), + free, + "reporting no move means the requested port is the bound one" + ); + return; + } + // The port was taken between the probe and the bind, so this + // attempt exercised the fallback path instead. That path has its + // own test; try again for a genuinely free port. + assert_eq!(requested, Some(free)); + } + panic!("the requested port was taken on all {ATTEMPTS} attempts"); + } + + #[tokio::test] + async fn refuses_to_forward_without_an_ssh_manager() { + let manager = PortForwardManager::new(); + let error = manager + .start_local_forward(&PortForwardRequest { + connection_id: "conn".to_string(), + remote_port: 3000, + remote_host: None, + local_port: None, + expose_on_lan: false, + label: None, + }) + .await + .expect_err("an uninitialized manager cannot forward"); + assert!(error.to_string().contains("not initialized"), "{error}"); + } + + #[tokio::test] + async fn rejects_a_request_with_no_remote_port() { + let manager = PortForwardManager::new(); + let error = manager + .start_local_forward(&PortForwardRequest { + connection_id: "conn".to_string(), + remote_port: 0, + remote_host: None, + local_port: None, + expose_on_lan: false, + label: None, + }) + .await + .expect_err("port 0 is not a service"); + assert!(error.to_string().contains("remote port"), "{error}"); + } + + #[tokio::test] + async fn stopping_an_unknown_forward_succeeds() { + let manager = PortForwardManager::new(); + manager + .stop_forward("does-not-exist") + .await + .expect("stop should be idempotent"); + assert!(manager.list_forwards().await.is_empty()); + } + + #[test] + fn request_defaults_target_loopback_on_both_ends() { + let request = PortForwardRequest { + connection_id: "conn".to_string(), + remote_port: 3000, + remote_host: Some(" ".to_string()), + local_port: Some(0), + expose_on_lan: false, + label: Some(" ".to_string()), + }; + assert_eq!(request.effective_remote_host(), "127.0.0.1"); + assert_eq!(request.effective_local_host(), "127.0.0.1"); + assert_eq!(request.preferred_local_port(), None); + assert_eq!(request.normalized_label(), None); + } + + #[test] + fn exposing_on_lan_is_the_only_way_to_leave_loopback() { + let request = PortForwardRequest { + connection_id: "conn".to_string(), + remote_port: 3000, + remote_host: None, + local_port: None, + expose_on_lan: true, + label: None, + }; + assert_eq!(request.effective_local_host(), "0.0.0.0"); + } + + #[test] + fn wildcard_binds_are_displayed_as_a_reachable_address() { + let forward = PortForward { + id: "id".to_string(), + connection_id: "conn".to_string(), + direction: PortForwardDirection::Local, + label: None, + local_host: "0.0.0.0".to_string(), + local_port: 3001, + requested_local_port: Some(3000), + remote_host: "127.0.0.1".to_string(), + remote_port: 3000, + active_connections: 0, + total_connections: 0, + last_error: None, + }; + assert_eq!(forward.local_address(), "127.0.0.1:3001"); + } +} diff --git a/src/crates/services/services-integrations/src/remote_ssh/types.rs b/src/crates/services/services-integrations/src/remote_ssh/types.rs index 783fbd7577..ed32593794 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/types.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/types.rs @@ -588,3 +588,136 @@ pub struct SSHConfigLookupResult { /// Config entry if found pub config: Option, } + +/// Direction of an SSH port forward. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PortForwardDirection { + /// `ssh -L`: a listener on this machine whose connections are carried to + /// the remote end. + Local, + /// `ssh -R`: a listener on the remote whose connections are carried back + /// to this machine. + Remote, + /// `ssh -D`: a local SOCKS proxy. + Dynamic, +} + +/// A user-requested local port forward. +/// +/// Only the remote port is required. Everything else has a default that suits +/// the common case: a dev server bound to the remote's loopback interface, +/// reachable from this machine on the same port number when it happens to be +/// free. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortForwardRequest { + /// SSH connection that carries the forward. + pub connection_id: String, + /// Port the service listens on, as seen from the remote host. + pub remote_port: u16, + /// Address used by the remote sshd to reach the service. Defaults to + /// loopback, which is what a server bound to `localhost` needs. + #[serde(default)] + pub remote_host: Option, + /// Port to bind on this machine. `None` or `0` allocates a free one. + #[serde(default)] + pub local_port: Option, + /// Bind every interface instead of loopback only. + /// + /// This republishes the remote service to the local network, so it stays + /// opt-in and is never implied by any other field. + #[serde(default)] + pub expose_on_lan: bool, + /// Optional user-facing name for the mapping. + #[serde(default)] + pub label: Option, +} + +impl PortForwardRequest { + /// Address the remote sshd should dial, defaulting to loopback. + pub fn effective_remote_host(&self) -> &str { + match self.remote_host.as_deref().map(str::trim) { + Some(host) if !host.is_empty() => host, + _ => "127.0.0.1", + } + } + + /// Interface to bind on this machine. + pub fn effective_local_host(&self) -> &'static str { + if self.expose_on_lan { + "0.0.0.0" + } else { + "127.0.0.1" + } + } + + /// Requested local port, with `0` normalized to "allocate one for me". + pub fn preferred_local_port(&self) -> Option { + self.local_port.filter(|port| *port != 0) + } + + /// Trimmed label, dropping a blank one. + pub fn normalized_label(&self) -> Option { + self.label + .as_deref() + .map(str::trim) + .filter(|label| !label.is_empty()) + .map(str::to_string) + } +} + +/// A live port forward as reported to the UI. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortForward { + pub id: String, + pub connection_id: String, + pub direction: PortForwardDirection, + pub label: Option, + /// Interface the listener is bound to on this machine. + pub local_host: String, + /// Port actually bound, which is not always the port that was asked for. + pub local_port: u16, + /// Set only when the requested port was unavailable and a different one was + /// bound instead. The UI needs this to explain the mismatch rather than + /// silently handing back an address the user did not ask for. + pub requested_local_port: Option, + pub remote_host: String, + pub remote_port: u16, + /// Connections currently being carried. + pub active_connections: u32, + /// Connections accepted since the forward started. + pub total_connections: u64, + /// Most recent per-connection failure. Kept as the forward's health signal: + /// a forward stays listening even when the remote service is not up yet. + pub last_error: Option, +} + +impl PortForward { + /// Address to hand a browser, in `host:port` form. + /// + /// A wildcard bind is displayed as loopback because that is the address + /// that actually works on this machine. + pub fn local_address(&self) -> String { + let host = match self.local_host.as_str() { + "0.0.0.0" => "127.0.0.1", + "::" => "[::1]", + other => other, + }; + format!("{}:{}", host, self.local_port) + } +} + +/// A listening TCP port discovered on the remote host. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteListeningPort { + pub port: u16, + /// Address the remote service is bound to, as reported by the remote. + pub bind_address: String, + /// Owning process name when the remote tool exposed it. + pub process: Option, + /// Owning process id when the remote tool exposed it. + pub pid: Option, +} diff --git a/src/crates/services/services-integrations/tests/remote_ssh_contracts/remote_ssh_contracts.rs b/src/crates/services/services-integrations/tests/remote_ssh_contracts/remote_ssh_contracts.rs index 4906fc3e69..3e00837143 100644 --- a/src/crates/services/services-integrations/tests/remote_ssh_contracts/remote_ssh_contracts.rs +++ b/src/crates/services/services-integrations/tests/remote_ssh_contracts/remote_ssh_contracts.rs @@ -6,7 +6,8 @@ use bitfun_services_integrations::remote_ssh::{ sanitize_remote_mirror_path_component, sanitize_ssh_connection_id_for_local_dir, sanitize_ssh_hostname_for_mirror, unresolved_remote_session_storage_dir, unresolved_remote_session_storage_key, workspace_logical_key, workspace_session_identity, - ContainerAccess, ContainerWorkspaceConfig, RemoteWorkspace, RemoteWorkspaceRegistry, + ContainerAccess, ContainerWorkspaceConfig, PortForward, PortForwardDirection, + PortForwardRequest, RemoteListeningPort, RemoteWorkspace, RemoteWorkspaceRegistry, SSHAuthMethod, SSHConnectionConfig, SavedAuthType, SavedConnection, LOCAL_WORKSPACE_SSH_HOST, }; @@ -371,3 +372,85 @@ async fn remote_workspace_registry_preserves_legacy_state_and_clear_contract() { assert!(!registry.has_any().await); assert!(!registry.get_state().await.is_active); } + +#[test] +fn port_forward_request_defaults_keep_a_bare_remote_port_usable() { + // The UI is allowed to send nothing but the connection and the remote port; + // every other field has to default to the safe, common-case answer. + let request: PortForwardRequest = serde_json::from_value(serde_json::json!({ + "connectionId": "conn-1", + "remotePort": 3000 + })) + .expect("a minimal request must deserialize"); + + assert_eq!(request.effective_remote_host(), "127.0.0.1"); + assert_eq!( + request.effective_local_host(), + "127.0.0.1", + "a forward must never reach beyond loopback unless asked to" + ); + assert_eq!(request.preferred_local_port(), None); + assert_eq!(request.normalized_label(), None); + assert!(!request.expose_on_lan); +} + +#[test] +fn port_forward_request_honors_an_explicit_lan_exposure() { + let request: PortForwardRequest = serde_json::from_value(serde_json::json!({ + "connectionId": "conn-1", + "remotePort": 8080, + "remoteHost": "10.0.0.5", + "localPort": 18080, + "exposeOnLan": true, + "label": " staging api " + })) + .expect("a full request must deserialize"); + + assert_eq!(request.effective_remote_host(), "10.0.0.5"); + assert_eq!(request.effective_local_host(), "0.0.0.0"); + assert_eq!(request.preferred_local_port(), Some(18080)); + assert_eq!(request.normalized_label().as_deref(), Some("staging api")); +} + +#[test] +fn port_forward_wire_shape_stays_camel_case() { + let forward = PortForward { + id: "forward-1".to_string(), + connection_id: "conn-1".to_string(), + direction: PortForwardDirection::Local, + label: None, + local_host: "127.0.0.1".to_string(), + local_port: 3001, + requested_local_port: Some(3000), + remote_host: "127.0.0.1".to_string(), + remote_port: 3000, + active_connections: 2, + total_connections: 17, + last_error: None, + }; + + let value = serde_json::to_value(&forward).expect("a forward must serialize"); + assert_eq!(value["localPort"], 3001); + assert_eq!( + value["requestedLocalPort"], 3000, + "the UI needs the requested port to explain a moved mapping" + ); + assert_eq!(value["remotePort"], 3000); + assert_eq!(value["activeConnections"], 2); + assert_eq!(value["direction"], "local"); + assert_eq!(forward.local_address(), "127.0.0.1:3001"); +} + +#[test] +fn remote_listening_port_wire_shape_stays_camel_case() { + let port = RemoteListeningPort { + port: 5173, + bind_address: "127.0.0.1".to_string(), + process: Some("vite".to_string()), + pid: Some(4242), + }; + let value = serde_json::to_value(&port).expect("a listening port must serialize"); + assert_eq!(value["bindAddress"], "127.0.0.1"); + assert_eq!(value["process"], "vite"); + assert_eq!(value["pid"], 4242); +} diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index 8e959c469a..450325475b 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -1,6 +1,6 @@ import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import { createPortal } from 'react-dom'; -import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil } from 'lucide-react'; +import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil, Network } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button, @@ -57,6 +57,11 @@ import { const WorkspaceRelatedPathsDialog = lazy(() => import('./WorkspaceRelatedPathsDialog')); const WorkspaceProjectPermissionsDialog = lazy(() => import('./WorkspaceProjectPermissionsDialog')); +const PortForwardDialog = lazy(() => + import('@/features/ssh-remote/PortForwardDialog').then((module) => ({ + default: module.PortForwardDialog, + })) +); const WorkspaceSessionBatchModal = lazy(() => import('./WorkspaceSessionBatchModal')); const ScheduledJobsModal = lazy(() => import('@/app/components/scheduled-jobs/ScheduledJobsModal')); @@ -122,6 +127,7 @@ const WorkspaceItem: React.FC = ({ const [resetDialogOpen, setResetDialogOpen] = useState(false); const [relatedPathsDialogOpen, setRelatedPathsDialogOpen] = useState(false); const [projectPermissionsDialogOpen, setProjectPermissionsDialogOpen] = useState(false); + const [portForwardDialogOpen, setPortForwardDialogOpen] = useState(false); const [isDeletingAssistant, setIsDeletingAssistant] = useState(false); const [isResettingWorkspace, setIsResettingWorkspace] = useState(false); const [sessionsCollapsed, setSessionsCollapsed] = useState(false); @@ -525,6 +531,16 @@ const WorkspaceItem: React.FC = ({ setProjectPermissionsDialogOpen(true); }, []); + const handleOpenPortForward = useCallback(() => { + setMenuOpen(false); + setPortForwardDialogOpen(true); + }, []); + + // A forward is carried by this workspace's SSH session, so the entry belongs + // to the workspace that owns the connection rather than to a global menu. + const portForwardConnectionId = + isRemoteWorkspace(workspace) && workspace.connectionId ? workspace.connectionId : null; + const handleRequestRename = useCallback(() => { setMenuOpen(false); setRenameDialogOpen(true); @@ -917,7 +933,22 @@ const WorkspaceItem: React.FC = ({ {t('nav.workspaces.actions.manageProjectPermissions')} -
+ {portForwardConnectionId ? ( + + ) : null} +
); } @@ -1424,6 +1468,21 @@ const WorkspaceItem: React.FC = ({ {t('nav.scheduledJobs.open')} + {portForwardConnectionId ? ( + + ) : null}
+ ), + Input: React.forwardRef< + HTMLInputElement, + { + value?: string; + onChange?: React.ChangeEventHandler; + placeholder?: string; + 'data-testid'?: string; + } + >(({ value, onChange, placeholder, 'data-testid': testId }, ref) => ( + + )), + Checkbox: ({ + checked, + onChange, + label, + }: { + checked?: boolean; + onChange?: React.ChangeEventHandler; + label?: React.ReactNode; + }) => ( + + ), +})); + +function makeForward(overrides: Partial = {}): PortForward { + return { + id: 'forward-1', + connectionId: 'conn-1', + direction: 'local', + localHost: '127.0.0.1', + localPort: 3000, + remoteHost: '127.0.0.1', + remotePort: 3000, + activeConnections: 0, + totalConnections: 0, + ...overrides, + }; +} + +describe('PortForwardDialog', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + vi.clearAllMocks(); + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + sshApiMock.listPortForwards.mockResolvedValue([]); + sshApiMock.startPortForward.mockResolvedValue(makeForward()); + sshApiMock.stopPortForward.mockResolvedValue(undefined); + sshApiMock.listRemoteListeningPorts.mockResolvedValue([]); + systemApiMock.openExternal.mockResolvedValue(undefined); + systemApiMock.setClipboard.mockResolvedValue(undefined); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + async function renderDialog(): Promise { + await act(async () => { + root.render( + + ); + }); + await act(async () => { + await Promise.resolve(); + }); + } + + function setInput(testId: string, value: string): void { + const input = container.querySelector(`[data-testid="${testId}"]`); + if (!input) throw new Error(`missing input ${testId}`); + const setter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set; + act(() => { + setter?.call(input, value); + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + } + + function click(testId: string): void { + const button = container.querySelector(`[data-testid="${testId}"]`); + if (!button) throw new Error(`missing button ${testId}`); + act(() => { + button.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + } + + it('detects listening ports on open without forwarding any of them', async () => { + sshApiMock.listRemoteListeningPorts.mockResolvedValue([ + { port: 8899, bindAddress: '0.0.0.0', process: 'python3' }, + ]); + await renderDialog(); + + expect(sshApiMock.listRemoteListeningPorts).toHaveBeenCalledWith('conn-1'); + expect(container.textContent).toContain('8899'); + expect( + sshApiMock.startPortForward, + 'detection must never forward on its own' + ).not.toHaveBeenCalled(); + }); + + it('forwards a detected port in one click, letting the backend pick the local port', async () => { + sshApiMock.listRemoteListeningPorts.mockResolvedValue([ + { port: 8899, bindAddress: '0.0.0.0', process: 'python3' }, + ]); + await renderDialog(); + + await act(async () => { + container + .querySelector('[data-testid="ssh-port-forward-chip"][data-port="8899"]') + ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + + expect(sshApiMock.startPortForward).toHaveBeenCalledWith({ + connectionId: 'conn-1', + remotePort: 8899, + localPort: undefined, + exposeOnLan: false, + label: 'python3', + }); + }); + + it('does not offer to forward a port that is already forwarded', async () => { + sshApiMock.listRemoteListeningPorts.mockResolvedValue([ + { port: 3000, bindAddress: '127.0.0.1', process: 'node' }, + ]); + sshApiMock.listPortForwards.mockResolvedValue([makeForward({ remotePort: 3000 })]); + await renderDialog(); + + const chip = container.querySelector( + '[data-testid="ssh-port-forward-chip"][data-port="3000"]' + ); + expect(chip?.disabled).toBe(true); + }); + + it('sends only the remote port when the local port is left blank', async () => { + await renderDialog(); + setInput('ssh-port-forward-remote-port', '3000'); + + await act(async () => { + click('ssh-port-forward-add'); + await Promise.resolve(); + }); + + expect(sshApiMock.startPortForward).toHaveBeenCalledWith({ + connectionId: 'conn-1', + remotePort: 3000, + localPort: undefined, + exposeOnLan: false, + label: undefined, + }); + }); + + it('refuses to submit a port outside the valid range', async () => { + await renderDialog(); + setInput('ssh-port-forward-remote-port', '70000'); + + const add = container.querySelector( + '[data-testid="ssh-port-forward-add"]' + ); + expect(add?.disabled).toBe(true); + + await act(async () => { + click('ssh-port-forward-add'); + await Promise.resolve(); + }); + expect(sshApiMock.startPortForward).not.toHaveBeenCalled(); + }); + + it('surfaces the backend error instead of leaving the row silently missing', async () => { + sshApiMock.startPortForward.mockRejectedValue( + new Error('127.0.0.1:3000 is already forwarded to 127.0.0.1:3001') + ); + await renderDialog(); + setInput('ssh-port-forward-remote-port', '3000'); + + await act(async () => { + click('ssh-port-forward-add'); + await Promise.resolve(); + }); + + expect(container.textContent).toContain('already forwarded'); + }); + + it('keeps the typed values when the mapping was rejected', async () => { + sshApiMock.startPortForward.mockRejectedValue(new Error('nope')); + await renderDialog(); + setInput('ssh-port-forward-remote-port', '3000'); + + await act(async () => { + click('ssh-port-forward-add'); + await Promise.resolve(); + }); + + const input = container.querySelector( + '[data-testid="ssh-port-forward-remote-port"]' + ); + expect(input?.value, 'a rejected mapping must not clear what was typed').toBe('3000'); + }); + + it('tells the user when the mapping moved to a different local port', async () => { + sshApiMock.listPortForwards.mockResolvedValue([ + makeForward({ localPort: 3001, requestedLocalPort: 3000 }), + ]); + await renderDialog(); + + expect(container.textContent).toContain( + 'ssh.portForward.portMoved:{"requested":3000,"bound":3001}' + ); + }); + + it('hands the browser the bound local port, not the remote one', async () => { + sshApiMock.listPortForwards.mockResolvedValue([ + makeForward({ localPort: 3001, requestedLocalPort: 3000 }), + ]); + await renderDialog(); + + const open = container.querySelector( + '[aria-label="ssh.portForward.openInBrowser"]' + ); + act(() => { + open?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + + expect(systemApiMock.openExternal).toHaveBeenCalledWith('http://127.0.0.1:3001'); + }); + + it('reports a wildcard bind as a loopback address the user can actually open', async () => { + sshApiMock.listPortForwards.mockResolvedValue([ + makeForward({ localHost: '0.0.0.0', localPort: 8080 }), + ]); + await renderDialog(); + + const copy = container.querySelector( + '[aria-label="ssh.portForward.copyAddress"]' + ); + act(() => { + copy?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); + + expect(systemApiMock.setClipboard).toHaveBeenCalledWith('127.0.0.1:8080'); + }); + + it('stops a forward by id', async () => { + sshApiMock.listPortForwards.mockResolvedValue([makeForward({ id: 'forward-9' })]); + await renderDialog(); + + await act(async () => { + click('ssh-port-forward-stop'); + await Promise.resolve(); + }); + + expect(sshApiMock.stopPortForward).toHaveBeenCalledWith('forward-9'); + }); +}); diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx new file mode 100644 index 0000000000..5aafed002c --- /dev/null +++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx @@ -0,0 +1,492 @@ +/** + * Remote Port Forwarding Dialog + * + * Manual mappings only: a forward exists because the user asked for it. + * + * Detection runs on open, though — knowing what the remote is listening on is + * not the same as forwarding it, and making people type a port number they + * would otherwise have to go look up is friction with nothing behind it. One + * click on a detected port creates the mapping; the form below stays for the + * cases detection cannot cover (a specific local port, a non-loopback remote + * host). + * + * The table keeps the remote port and the local address in separate columns + * because they routinely differ: the remote port is what the user thinks in, + * the local port is an allocation that moves when the number is taken. + */ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Modal, Button, Input, Checkbox } from '@/component-library'; +import { useI18n } from '@/infrastructure/i18n'; +import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI'; +import { createLogger } from '@/shared/utils/logger'; +import { AlertTriangle, Copy, ExternalLink, Plus, RefreshCw, X, Check } from 'lucide-react'; +import { sshApi } from './sshApi'; +import type { PortForward, RemoteListeningPort } from './types'; +import './PortForwardDialog.scss'; + +const log = createLogger('PortForwardDialog'); + +/** + * How often the table refreshes its counters while the dialog is open. + * + * The backend keeps the numbers in atomics and snapshots on demand, so polling + * costs one cheap command; anything faster just makes the digits flicker. + */ +const REFRESH_INTERVAL_MS = 2000; + +interface PortForwardDialogProps { + open: boolean; + connectionId: string; + connectionName?: string; + onClose: () => void; +} + +/** Parse a port field, distinguishing "empty" from "invalid". */ +function parsePortInput(value: string): { port?: number; valid: boolean } { + const trimmed = value.trim(); + if (!trimmed) return { valid: true }; + if (!/^\d+$/.test(trimmed)) return { valid: false }; + const port = Number(trimmed); + if (port < 1 || port > 65535) return { valid: false }; + return { port, valid: true }; +} + +/** Address to hand a browser. A wildcard bind is only reachable here via loopback. */ +function localAddressOf(forward: PortForward): string { + const host = + forward.localHost === '0.0.0.0' + ? '127.0.0.1' + : forward.localHost === '::' + ? '[::1]' + : forward.localHost; + return `${host}:${forward.localPort}`; +} + +export const PortForwardDialog: React.FC = ({ + open, + connectionId, + connectionName, + onClose, +}) => { + const { t } = useI18n('common'); + + const [forwards, setForwards] = useState([]); + const [remotePort, setRemotePort] = useState(''); + const [localPort, setLocalPort] = useState(''); + const [label, setLabel] = useState(''); + const [exposeOnLan, setExposeOnLan] = useState(false); + const [error, setError] = useState(null); + const [busyPort, setBusyPort] = useState(null); + const [isStarting, setIsStarting] = useState(false); + const [detectedPorts, setDetectedPorts] = useState(null); + const [isDetecting, setIsDetecting] = useState(false); + const remotePortInputRef = useRef(null); + + const refresh = useCallback(async () => { + try { + setForwards(await sshApi.listPortForwards(connectionId)); + } catch (err) { + log.error('Failed to list port forwards', err); + } + }, [connectionId]); + + const detect = useCallback(async () => { + setIsDetecting(true); + try { + setDetectedPorts(await sshApi.listRemoteListeningPorts(connectionId)); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + setDetectedPorts([]); + } finally { + setIsDetecting(false); + } + }, [connectionId]); + + useEffect(() => { + if (!open) return; + void refresh(); + const timer = window.setInterval(() => { + void refresh(); + }, REFRESH_INTERVAL_MS); + return () => window.clearInterval(timer); + }, [open, refresh]); + + // Detect once per opening. Re-detecting on every poll would fight the user's + // cursor, and a listing captured minutes ago is worse than no listing. + useEffect(() => { + if (!open) return; + void detect(); + }, [open, detect]); + + useEffect(() => { + if (open) return; + setDetectedPorts(null); + setError(null); + }, [open]); + + const remotePortParsed = useMemo(() => parsePortInput(remotePort), [remotePort]); + const localPortParsed = useMemo(() => parsePortInput(localPort), [localPort]); + const canSubmit = + !isStarting && + remotePortParsed.valid && + remotePortParsed.port !== undefined && + localPortParsed.valid; + + const startForward = useCallback( + async (request: { + remotePort: number; + localPort?: number; + label?: string; + exposeOnLan?: boolean; + }) => { + setError(null); + try { + await sshApi.startPortForward({ + connectionId, + remotePort: request.remotePort, + localPort: request.localPort, + exposeOnLan: request.exposeOnLan ?? false, + label: request.label, + }); + await refresh(); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + return false; + } + }, + [connectionId, refresh] + ); + + const handleAdd = useCallback(async () => { + if (remotePortParsed.port === undefined || !localPortParsed.valid) return; + setIsStarting(true); + const ok = await startForward({ + remotePort: remotePortParsed.port, + localPort: localPortParsed.port, + label: label.trim() || undefined, + exposeOnLan, + }); + setIsStarting(false); + if (ok) { + setRemotePort(''); + setLocalPort(''); + setLabel(''); + remotePortInputRef.current?.focus(); + } + }, [exposeOnLan, label, localPortParsed, remotePortParsed.port, startForward]); + + /** One click on a detected port is still the user asking for the mapping. */ + const handleForwardDetected = useCallback( + async (port: RemoteListeningPort) => { + setBusyPort(port.port); + await startForward({ remotePort: port.port, label: port.process ?? undefined }); + setBusyPort(null); + }, + [startForward] + ); + + const handleStop = useCallback( + async (forwardId: string) => { + try { + await sshApi.stopPortForward(forwardId); + await refresh(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + }, + [refresh] + ); + + const handleOpen = useCallback((forward: PortForward) => { + void systemAPI + .openExternal(`http://${localAddressOf(forward)}`) + .catch((err) => log.error('Failed to open forwarded address', err)); + }, []); + + const handleCopy = useCallback((forward: PortForward) => { + void systemAPI + .setClipboard(localAddressOf(forward)) + .catch((err) => log.error('Failed to copy forwarded address', err)); + }, []); + + const forwardedRemotePorts = useMemo( + () => new Set(forwards.map((forward) => forward.remotePort)), + [forwards] + ); + + return ( + {connectionName} + ) : undefined + } + size="medium" + showCloseButton + testId="ssh-port-forward-dialog" + > +
+ {/* Discovery first: it answers "which port?" without making anyone look it up. */} +
+
+

{t('ssh.portForward.detectedTitle')}

+ +
+ + {isDetecting && detectedPorts === null ? ( +

{t('ssh.portForward.detecting')}

+ ) : detectedPorts && detectedPorts.length > 0 ? ( +
+ {detectedPorts.map((port) => { + const alreadyForwarded = forwardedRemotePorts.has(port.port); + return ( + + ); + })} +
+ ) : ( +

{t('ssh.portForward.detectedEmpty')}

+ )} +
+ + {error && ( +
+
+ )} + +
+
+

{t('ssh.portForward.activeTitle')}

+
+ + {forwards.length === 0 ? ( +

{t('ssh.portForward.empty')}

+ ) : ( +
+ + + + + + + + + + + {forwards.map((forward) => ( + + + + + + + + ))} + +
{t('ssh.portForward.columnRemotePort')}{t('ssh.portForward.columnLocalAddress')}{t('ssh.portForward.columnLabel')}{t('ssh.portForward.columnStatus')} +
+ {forward.remotePort} + {forward.remoteHost !== '127.0.0.1' && ( + {forward.remoteHost} + )} + + {localAddressOf(forward)} + {forward.requestedLocalPort !== undefined && + forward.requestedLocalPort !== null && ( + + {t('ssh.portForward.portMoved', { + requested: forward.requestedLocalPort, + bound: forward.localPort, + })} + + )} + {forward.localHost === '0.0.0.0' && ( + + {t('ssh.portForward.exposedOnLanBadge')} + + )} + {forward.label ?? '—'} + {forward.lastError ? ( + + + ) : ( + + {t('ssh.portForward.statusActive', { + active: forward.activeConnections, + })} + + )} + + + + +
+
+ )} +
+ + {/* The escape hatch: a specific local port, or a host detection cannot see. */} +
+
+

{t('ssh.portForward.manualTitle')}

+
+ +
+ + + + + + + +
+ + setExposeOnLan(event.target.checked)} + label={t('ssh.portForward.exposeOnLan')} + title={t('ssh.portForward.exposeOnLanHint')} + /> +
+
+
+ ); +}; + +export default PortForwardDialog; diff --git a/src/web-ui/src/features/ssh-remote/appearance.ts b/src/web-ui/src/features/ssh-remote/appearance.ts index 203c19fa25..81b29e814c 100644 --- a/src/web-ui/src/features/ssh-remote/appearance.ts +++ b/src/web-ui/src/features/ssh-remote/appearance.ts @@ -27,5 +27,9 @@ export const sshRemoteAppearanceDescriptor: AppearanceSurfaceDescriptor = { { id: 'confirmWarning' }, { id: 'confirmMessage' }, { id: 'confirmActions' }, + { id: 'portForward' }, + { id: 'portForwardForm' }, + { id: 'portForwardError' }, + { id: 'portForwardTable' }, ], }; diff --git a/src/web-ui/src/features/ssh-remote/index.ts b/src/web-ui/src/features/ssh-remote/index.ts index 0a1685ae22..ccc750a63d 100644 --- a/src/web-ui/src/features/ssh-remote/index.ts +++ b/src/web-ui/src/features/ssh-remote/index.ts @@ -8,5 +8,6 @@ export { SSHConnectionDialog } from './SSHConnectionDialog'; export { RemoteFileBrowser } from './RemoteFileBrowser'; export { SSHAuthPromptDialog } from './SSHAuthPromptDialog'; export { ConfirmDialog } from './ConfirmDialog'; +export { PortForwardDialog } from './PortForwardDialog'; export { SSHRemoteProvider } from './SSHRemoteProvider'; export { useSSHRemoteContext } from './SSHRemoteContext'; diff --git a/src/web-ui/src/features/ssh-remote/sshApi.ts b/src/web-ui/src/features/ssh-remote/sshApi.ts index e6e8494599..d82da1789c 100644 --- a/src/web-ui/src/features/ssh-remote/sshApi.ts +++ b/src/web-ui/src/features/ssh-remote/sshApi.ts @@ -14,6 +14,9 @@ import type { ServerInfo, DockerContainerInfo, ConnectionTestReport, + PortForward, + PortForwardRequest, + RemoteListeningPort, } from './types'; // API adapter for Tauri/Server Mode compatibility @@ -325,4 +328,38 @@ export const sshApi = { async getWorkspaceInfo(): Promise { return api.invoke('remote_get_workspace_info', {}); }, + + // === Port Forwarding === + + /** + * Start a local (`-L`) forward. + * + * The resolved mapping comes back rather than the request: an unavailable + * local port is replaced instead of refused, so `localPort` is authoritative + * and `requestedLocalPort` records the move. + */ + async startPortForward(request: PortForwardRequest): Promise { + return api.invoke('ssh_start_port_forward', { request }); + }, + + /** Stop one forward. Stopping an already-gone forward is not an error. */ + async stopPortForward(forwardId: string): Promise { + return api.invoke('ssh_stop_port_forward', { forwardId }); + }, + + /** List forwards, optionally narrowed to one connection. */ + async listPortForwards(connectionId?: string): Promise { + return api.invoke('ssh_list_port_forwards', { + connectionId: connectionId ?? null, + }); + }, + + /** + * List TCP ports currently listening on the remote host. + * + * Discovery only — nothing is forwarded as a result. + */ + async listRemoteListeningPorts(connectionId: string): Promise { + return api.invoke('ssh_list_remote_listening_ports', { connectionId }); + }, }; diff --git a/src/web-ui/src/features/ssh-remote/types.ts b/src/web-ui/src/features/ssh-remote/types.ts index f8ce73446e..696c67bff9 100644 --- a/src/web-ui/src/features/ssh-remote/types.ts +++ b/src/web-ui/src/features/ssh-remote/types.ts @@ -135,3 +135,45 @@ export interface ConnectionTestReport { serverInfo?: ServerInfo; resolvedContainerAccess?: ContainerAccess; } + +export type PortForwardDirection = 'local' | 'remote' | 'dynamic'; + +/** A mapping the user asked for. Only `connectionId` and `remotePort` are required. */ +export interface PortForwardRequest { + connectionId: string; + /** Port the service listens on, as seen from the remote host. */ + remotePort: number; + /** Address the remote uses to reach the service. Defaults to `127.0.0.1`. */ + remoteHost?: string; + /** Local port to bind. Omitted or `0` lets the backend allocate one. */ + localPort?: number; + /** Bind every local interface instead of loopback only. Republishes the remote service to the LAN. */ + exposeOnLan?: boolean; + label?: string; +} + +/** A live mapping. `localPort` is what was actually bound, not what was asked for. */ +export interface PortForward { + id: string; + connectionId: string; + direction: PortForwardDirection; + label?: string; + localHost: string; + localPort: number; + /** Set only when the requested port was taken and a different one was bound instead. */ + requestedLocalPort?: number; + remoteHost: string; + remotePort: number; + activeConnections: number; + totalConnections: number; + /** Most recent per-connection failure; a forward keeps listening through one. */ + lastError?: string; +} + +/** A TCP port found listening on the remote host. */ +export interface RemoteListeningPort { + port: number; + bindAddress: string; + process?: string; + pid?: number; +} diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index d41a11444e..68fa8030fb 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -1685,6 +1685,39 @@ "transferFailed": "Transfer failed", "transferNeedsDesktop": "Upload and download require the desktop app.", "transferring": "Transferring..." + }, + "portForward": { + "menuEntry": "Forward a Port", + "title": "Port Forwarding", + "detectedTitle": "Listening on the remote", + "detecting": "Detecting remote ports…", + "detect": "Detect again", + "detectedEmpty": "No listening ports found on the remote host. Add one manually below.", + "detectedUse": "Forward to this machine", + "detectedAlreadyForwarded": "This port is already forwarded", + "activeTitle": "Forwarded", + "empty": "No port mappings yet. Pick a detected port above, or add one manually below.", + "columnRemotePort": "Remote port", + "columnLocalAddress": "Local address", + "columnLabel": "Label", + "columnStatus": "Status", + "columnActions": "Actions", + "portMoved": "Local {{requested}} was in use; bound {{bound}} instead", + "exposedOnLanBadge": "Reachable on the LAN", + "statusActive": "{{active}} connection(s)", + "statusWarning": "Error", + "openInBrowser": "Open in browser", + "copyAddress": "Copy local address", + "stop": "Stop forwarding", + "manualTitle": "Add manually", + "remotePortLabel": "Remote port", + "localPortLabel": "Local port", + "localPortPlaceholder": "Auto", + "labelLabel": "Label", + "labelPlaceholder": "Optional, e.g. web preview", + "add": "Add mapping", + "exposeOnLan": "Allow access from the local network", + "exposeOnLanHint": "Binds every local interface, so other devices on your network can reach this remote service too. Loopback only by default." } }, "notify": { diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index ede9d0c3d1..ee14eee845 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -1685,6 +1685,39 @@ "transferFailed": "传输失败", "transferNeedsDesktop": "上传与下载仅适用于桌面版应用。", "transferring": "正在传输…" + }, + "portForward": { + "menuEntry": "端口映射", + "title": "端口映射", + "detectedTitle": "远程正在监听", + "detecting": "正在检测远程端口…", + "detect": "重新检测", + "detectedEmpty": "远程主机上没有检测到正在监听的端口,可以在下方手动填写。", + "detectedUse": "映射到本机", + "detectedAlreadyForwarded": "这个端口已经映射过了", + "activeTitle": "已映射", + "empty": "还没有端口映射。点上面检测到的端口,或在下方手动填写。", + "columnRemotePort": "远程端口", + "columnLocalAddress": "本地地址", + "columnLabel": "备注", + "columnStatus": "状态", + "columnActions": "操作", + "portMoved": "本地 {{requested}} 被占用,已改用 {{bound}}", + "exposedOnLanBadge": "局域网可访问", + "statusActive": "{{active}} 个连接", + "statusWarning": "有错误", + "openInBrowser": "在浏览器中打开", + "copyAddress": "复制本地地址", + "stop": "停止映射", + "manualTitle": "手动添加", + "remotePortLabel": "远程端口", + "localPortLabel": "本地端口", + "localPortPlaceholder": "自动", + "labelLabel": "备注", + "labelPlaceholder": "选填,例如 前端预览", + "add": "添加映射", + "exposeOnLan": "允许局域网访问", + "exposeOnLanHint": "绑定本机所有网卡,同一网络下的其他设备也能访问这个远程服务。默认只绑定 127.0.0.1。" } }, "notify": { diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 12c7c82150..96e631c209 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -1685,6 +1685,39 @@ "transferFailed": "傳輸失敗", "transferNeedsDesktop": "上傳與下載僅適用於桌面版應用。", "transferring": "正在傳輸…" + }, + "portForward": { + "menuEntry": "連接埠對應", + "title": "連接埠對應", + "detectedTitle": "遠端正在監聽", + "detecting": "正在偵測遠端連接埠…", + "detect": "重新偵測", + "detectedEmpty": "遠端主機上沒有偵測到正在監聽的連接埠,可以在下方手動填寫。", + "detectedUse": "對應到本機", + "detectedAlreadyForwarded": "這個連接埠已經對應過了", + "activeTitle": "已對應", + "empty": "還沒有連接埠對應。點上面偵測到的連接埠,或在下方手動填寫。", + "columnRemotePort": "遠端連接埠", + "columnLocalAddress": "本機位址", + "columnLabel": "備註", + "columnStatus": "狀態", + "columnActions": "操作", + "portMoved": "本機 {{requested}} 已被占用,改用 {{bound}}", + "exposedOnLanBadge": "區域網路可存取", + "statusActive": "{{active}} 個連線", + "statusWarning": "有錯誤", + "openInBrowser": "在瀏覽器中開啟", + "copyAddress": "複製本機位址", + "stop": "停止對應", + "manualTitle": "手動新增", + "remotePortLabel": "遠端連接埠", + "localPortLabel": "本機連接埠", + "localPortPlaceholder": "自動", + "labelLabel": "備註", + "labelPlaceholder": "選填,例如 前端預覽", + "add": "新增對應", + "exposeOnLan": "允許區域網路存取", + "exposeOnLanHint": "綁定本機所有網路介面,同一網路下的其他裝置也能存取這個遠端服務。預設只綁定 127.0.0.1。" } }, "notify": { From 93c19e4ae7f5a3f8033d5cfd9cf99f07aa97edf5 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Thu, 20 Aug 2026 23:53:02 -0700 Subject: [PATCH 2/5] fix(remote-ssh): gate the PortForward tool on remote SSH and restyle its dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems with the previous commit. The tool module compiled unconditionally but its imports only exist under `remote-workspace`. `bitfun-sdk-host` enables `tools-agent-control` without it, so the SDK host build broke — a workspace-wide `cargo check` missed it because feature unification hands `bitfun-core` the union of every consumer's features. Gate the SSH-dependent parts and report the tool as unavailable when the build has no SSH transport, rather than gating the provider plan: registry construction treats an unmaterialized tool name as a hard error, so the name has to stay resolvable in every build. A tool that could only fail is hidden instead of offered. The dialog also ignored the modal conventions every other workspace dialog follows, most visibly `contentInset` — without it the content sat flush against the modal edge with no padding at all. Adopt the same structure as the sibling dialogs on this menu: an intro callout, bordered section cards with tinted headers, library `IconButton`s for row actions, rows separated by a rule instead of a bare table, and font sizes from the appearance tokens rather than hardcoded pixels. --- .../implementations/port_forward_tool.rs | 31 +- .../ssh-remote/PortForwardDialog.scss | 268 +++++----- .../ssh-remote/PortForwardDialog.test.tsx | 22 + .../features/ssh-remote/PortForwardDialog.tsx | 479 +++++++++--------- .../src/features/ssh-remote/appearance.ts | 1 + src/web-ui/src/locales/en-US/common.json | 1 + src/web-ui/src/locales/zh-CN/common.json | 1 + src/web-ui/src/locales/zh-TW/common.json | 1 + 8 files changed, 443 insertions(+), 361 deletions(-) diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs index b0f87f5366..5ff1b85737 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/port_forward_tool.rs @@ -11,6 +11,7 @@ use crate::agentic::tools::framework::{ PermissionIntent, Tool, ToolExposure, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, }; +#[cfg(feature = "remote-workspace")] use crate::service::remote_ssh::{ global_port_forward_manager, list_remote_listening_ports, PortForwardRequest, SSHAuthMethod, SSHConnectionConfig, SSHConnectionManager, SSHConnectionOptions, @@ -60,6 +61,7 @@ impl PortForwardToolInput { } /// An SSH endpoint parsed out of a `[user@]host[:port]` string. +#[cfg_attr(not(feature = "remote-workspace"), allow(dead_code))] struct ParsedEndpoint { user: Option, host: String, @@ -95,6 +97,7 @@ fn parse_endpoint(target: &str) -> Option { }) } +#[cfg(feature = "remote-workspace")] fn current_username() -> String { std::env::var("USER") .or_else(|_| std::env::var("USERNAME")) @@ -109,6 +112,7 @@ impl PortForwardTool { } /// Connection bound to this session, when there is one. + #[cfg(feature = "remote-workspace")] fn workspace_connection_id(context: &ToolUseContext) -> Option { let workspace = context.workspace.as_ref()?; if !workspace.is_remote() { @@ -122,6 +126,7 @@ impl PortForwardTool { /// Saved connections win over `~/.ssh/config` so an explicitly configured /// profile (with its stored credentials) is preferred over re-deriving one, /// and a bare endpoint is the last resort. + #[cfg(feature = "remote-workspace")] async fn resolve_target( manager: &SSHConnectionManager, target: Option<&str>, @@ -168,6 +173,7 @@ impl PortForwardTool { /// The id is derived from the endpoint so repeated calls reuse one session /// instead of stacking a new one per tool call. Nothing is persisted to the /// user's saved connections. + #[cfg(feature = "remote-workspace")] async fn connect_ad_hoc(manager: &SSHConnectionManager, target: &str) -> BitFunResult { let endpoint = parse_endpoint(target).ok_or_else(|| { BitFunError::tool(format!( @@ -298,6 +304,14 @@ Forwards bind 127.0.0.1 and last until stopped or the connection drops. Set expo ToolExposure::Deferred } + /// Hidden in builds without remote SSH support. The tool stays in the + /// provider plan so registry construction is total, but a build with no SSH + /// transport has nothing for it to forward over, and offering a tool that + /// can only fail is worse than not offering it. + async fn is_available_in_context(&self, _context: Option<&ToolUseContext>) -> bool { + cfg!(feature = "remote-workspace") + } + fn input_schema(&self) -> Value { json!({ "type": "object", @@ -437,6 +451,18 @@ Forwards bind 127.0.0.1 and last until stopped or the connection drops. Set expo } } + #[cfg(not(feature = "remote-workspace"))] + async fn call_impl( + &self, + _input: &Value, + _context: &ToolUseContext, + ) -> BitFunResult> { + Err(BitFunError::tool( + "This build has no remote SSH support, so ports cannot be forwarded".to_string(), + )) + } + + #[cfg(feature = "remote-workspace")] async fn call_impl( &self, input: &Value, @@ -568,6 +594,7 @@ Forwards bind 127.0.0.1 and last until stopped or the connection drops. Set expo } /// Reach the SSH manager that owns this process's connections. +#[cfg(feature = "remote-workspace")] async fn resolve_ssh_manager() -> BitFunResult { let manager = crate::service::remote_ssh::workspace_state::get_remote_workspace_manager() .ok_or_else(|| BitFunError::tool("Remote SSH services are not initialized".to_string()))?; @@ -614,10 +641,12 @@ mod tests { } } + #[cfg(feature = "remote-workspace")] #[tokio::test] async fn the_tool_is_offered_outside_remote_workspaces() { // A session that reached a host with `ssh` in a shell still needs to - // forward its ports, so availability must not depend on the workspace. + // forward its ports, so availability must not depend on the workspace — + // only on the build carrying an SSH transport at all. let tool = PortForwardTool::new(); assert!(tool.is_available_in_context(None).await); } diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.scss b/src/web-ui/src/features/ssh-remote/PortForwardDialog.scss index b26c9c97e4..7486cb9f23 100644 --- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.scss +++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.scss @@ -1,83 +1,109 @@ /** * Remote port forwarding dialog + * + * Follows the workspace dialog conventions: `contentInset` on the modal, + * bordered section cards with a tinted header, and rows separated by a rule + * rather than a table. */ -.ssh-port-forward { +.port-forward-dialog { display: flex; flex-direction: column; - gap: 18px; - padding: 4px 0 2px; + gap: 16px; + min-width: 0; + padding-block: 4px 16px; + color: var(--bf-appearance-token-color-text-primary); + + &__modal { + overflow-x: hidden; + } &__target { - font-size: 12px; - font-family: var(--bf-appearance-token-font-family-mono); color: var(--bf-appearance-token-color-text-muted); + font-family: var(--bf-appearance-token-font-family-mono); + font-size: var(--bf-appearance-token-font-size-xs); } - &__section { + &__intro { display: flex; - flex-direction: column; + align-items: flex-start; gap: 8px; + padding: 8px 10px; + border: 1px solid var(--bf-appearance-token-border-subtle); + border-radius: 6px; + background: var(--bf-appearance-token-element-bg-subtle); + color: var(--bf-appearance-token-color-text-secondary); + font-size: var(--bf-appearance-token-font-size-xs); + line-height: 1.5; - &--manual { - padding-top: 14px; - border-top: 1px solid var(--bf-appearance-token-border-subtle); + svg { + flex-shrink: 0; + margin-top: 1px; + color: var(--bf-appearance-token-color-accent-500); + } + + p { + margin: 0; } } - &__section-head { + &__error { display: flex; - align-items: center; - gap: 6px; - min-height: 20px; + align-items: flex-start; + gap: 8px; + padding: 8px 10px; + border: 1px solid var(--bf-appearance-token-color-error-border); + border-radius: 6px; + background: var(--bf-appearance-token-color-error-bg); + color: var(--bf-appearance-token-color-error); + font-size: var(--bf-appearance-token-font-size-xs); + line-height: 1.5; - h4 { - margin: 0; - font-size: 11px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--bf-appearance-token-color-text-muted); + svg { + flex-shrink: 0; + margin-top: 1px; } } - &__muted { - margin: 0; - font-size: 12px; - line-height: 1.5; - color: var(--bf-appearance-token-color-text-muted); + &__section { + display: flex; + flex-direction: column; + min-width: 0; + border: 1px solid var(--bf-appearance-token-border-subtle); + border-radius: 8px; + overflow: hidden; } - &__icon-button { - display: inline-flex; + &__section-header { + display: flex; align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border: none; - border-radius: 5px; - background: transparent; - color: var(--bf-appearance-token-color-text-muted); - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - - &:hover:not(:disabled) { - background: var(--bf-appearance-token-element-bg-hover); - color: var(--bf-appearance-token-color-text-primary); - } + justify-content: space-between; + gap: 12px; + min-height: 44px; + padding: 8px 12px; + background: var(--bf-appearance-token-element-bg-subtle); + color: var(--bf-appearance-token-color-text-primary); + font-size: var(--bf-appearance-token-font-size-sm); + font-weight: 600; + } - &--danger:hover:not(:disabled) { - color: var(--bf-appearance-token-color-error); - } + &__section-body { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + min-width: 0; + } - &:disabled { - opacity: 0.5; - cursor: default; - } + &__empty { + margin: 0; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-xs); + line-height: 1.5; } &__spin { - animation: ssh-port-forward-spin 1s linear infinite; + animation: port-forward-dialog-spin 1s linear infinite; } &__chips { @@ -91,11 +117,11 @@ align-items: center; gap: 5px; padding: 4px 10px 4px 7px; - border-radius: 6px; border: 1px solid var(--bf-appearance-token-border-subtle); + border-radius: 6px; background: var(--bf-appearance-token-element-bg-base); color: var(--bf-appearance-token-color-text-primary); - font-size: 12px; + font-size: var(--bf-appearance-token-font-size-xs); cursor: pointer; transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; @@ -106,8 +132,8 @@ } &[data-state='forwarded'] { - color: var(--bf-appearance-token-color-text-muted); background: var(--bf-appearance-token-element-bg-subtle); + color: var(--bf-appearance-token-color-text-muted); } &:disabled { @@ -129,100 +155,76 @@ color: var(--bf-appearance-token-color-text-muted); } - &__error { + &__rows { display: flex; - align-items: flex-start; - gap: 8px; - padding: 9px 11px; - border-radius: 6px; - background: var(--bf-appearance-token-color-error-bg); - border: 1px solid var(--bf-appearance-token-color-error-border); - color: var(--bf-appearance-token-color-error); - font-size: 12px; - line-height: 1.45; - - svg { - flex-shrink: 0; - margin-top: 1px; - } + flex-direction: column; + min-width: 0; } - &__table { - border: 1px solid var(--bf-appearance-token-border-subtle); - border-radius: 6px; - overflow: hidden; - - table { - width: 100%; - border-collapse: collapse; - font-size: 12px; - } - - th { - text-align: left; - padding: 7px 12px; - font-weight: 500; - color: var(--bf-appearance-token-color-text-muted); - background: var(--bf-appearance-token-element-bg-subtle); - border-bottom: 1px solid var(--bf-appearance-token-border-subtle); - white-space: nowrap; - } - - td { - padding: 8px 12px; - border-bottom: 1px solid var(--bf-appearance-token-border-subtle); - color: var(--bf-appearance-token-color-text-primary); - vertical-align: top; - } + &__row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + min-width: 0; + padding: 10px 12px; - tr:last-child td { - border-bottom: none; + + .port-forward-dialog__row { + border-top: 1px solid var(--bf-appearance-token-border-subtle); } } - &__cell-port { - font-family: var(--bf-appearance-token-font-family-mono); - white-space: nowrap; - } - - &__cell-host { - display: block; - font-size: 11px; - color: var(--bf-appearance-token-color-text-muted); + &__row-copy { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + min-width: 0; } - &__cell-address code { + &__remote, + &__local { font-family: var(--bf-appearance-token-font-family-mono); - font-size: 12px; + font-size: var(--bf-appearance-token-font-size-xs); } - &__cell-label { + &__remote { color: var(--bf-appearance-token-color-text-secondary); - word-break: break-word; } - &__moved, - &__lan { - display: block; - margin-top: 3px; - font-size: 11px; - line-height: 1.4; + &__local { + color: var(--bf-appearance-token-color-text-primary); + font-weight: 500; } - &__moved { - color: var(--bf-appearance-token-color-warning); + &__arrow { + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-xs); } - &__lan { - color: var(--bf-appearance-token-color-text-muted); + &__meta { + display: inline-flex; + align-items: center; + gap: 8px; + margin-left: auto; + } + + &__label { + color: var(--bf-appearance-token-color-text-secondary); + font-size: var(--bf-appearance-token-font-size-xs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; } &__status { display: inline-flex; align-items: center; gap: 4px; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-xs); white-space: nowrap; - color: var(--bf-appearance-token-color-text-secondary); &--warn { color: var(--bf-appearance-token-color-error); @@ -230,10 +232,21 @@ } } - &__cell-actions { + &__note { + flex-basis: 100%; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-2xs); + line-height: 1.4; + + &--warn { + color: var(--bf-appearance-token-color-warning); + } + } + + &__row-actions { display: flex; - gap: 1px; - justify-content: flex-end; + align-items: center; + gap: 2px; } &__form { @@ -241,17 +254,18 @@ align-items: flex-end; flex-wrap: wrap; gap: 8px; + min-width: 0; } &__field { display: flex; flex-direction: column; gap: 5px; - width: 96px; + width: 100px; > span { - font-size: 11px; color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-xs); } &--grow { @@ -262,7 +276,7 @@ } } -@keyframes ssh-port-forward-spin { +@keyframes port-forward-dialog-spin { to { transform: rotate(360deg); } diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx index dd2f626e64..93109b0b63 100644 --- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx +++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.test.tsx @@ -55,6 +55,28 @@ vi.mock('@/component-library', () => ({ {children} ), + IconButton: ({ + children, + onClick, + disabled, + 'aria-label': ariaLabel, + 'data-testid': testId, + }: React.PropsWithChildren<{ + onClick?: React.MouseEventHandler; + disabled?: boolean; + 'aria-label'?: string; + 'data-testid'?: string; + }>) => ( + + ), Input: React.forwardRef< HTMLInputElement, { diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx index 5aafed002c..ac044345c8 100644 --- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx +++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx @@ -10,17 +10,17 @@ * cases detection cannot cover (a specific local port, a non-loopback remote * host). * - * The table keeps the remote port and the local address in separate columns + * The rows keep the remote port and the local address in separate columns * because they routinely differ: the remote port is what the user thinks in, * the local port is an allocation that moves when the number is taken. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Modal, Button, Input, Checkbox } from '@/component-library'; +import { Modal, Button, IconButton, Input, Checkbox } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI'; import { createLogger } from '@/shared/utils/logger'; -import { AlertTriangle, Copy, ExternalLink, Plus, RefreshCw, X, Check } from 'lucide-react'; +import { AlertTriangle, Check, Copy, ExternalLink, Network, Plus, RefreshCw, X } from 'lucide-react'; import { sshApi } from './sshApi'; import type { PortForward, RemoteListeningPort } from './types'; import './PortForwardDialog.scss'; @@ -28,7 +28,7 @@ import './PortForwardDialog.scss'; const log = createLogger('PortForwardDialog'); /** - * How often the table refreshes its counters while the dialog is open. + * How often the rows refresh their counters while the dialog is open. * * The backend keeps the numbers in atomics and snapshots on demand, so polling * costs one cheap command; anything faster just makes the digits flicker. @@ -224,265 +224,278 @@ export const PortForwardDialog: React.FC = ({ title={t('ssh.portForward.title')} titleExtra={ connectionName ? ( - {connectionName} + {connectionName} ) : undefined } - size="medium" + size="large" + contentInset + contentClassName="port-forward-dialog__modal" showCloseButton testId="ssh-port-forward-dialog" > -
+
+
+
+ + {error && ( +
+
+ )} + {/* Discovery first: it answers "which port?" without making anyone look it up. */} -
-
-

{t('ssh.portForward.detectedTitle')}

- -
- - {isDetecting && detectedPorts === null ? ( -

{t('ssh.portForward.detecting')}

- ) : detectedPorts && detectedPorts.length > 0 ? ( -
- {detectedPorts.map((port) => { - const alreadyForwarded = forwardedRemotePorts.has(port.port); - return ( - - ); - })} -
- ) : ( -

{t('ssh.portForward.detectedEmpty')}

- )} -
+ +
- {error && ( -
-
+ ))}
)} {/* The escape hatch: a specific local port, or a host detection cannot see. */} -
-
-

{t('ssh.portForward.manualTitle')}

-
+
+
+ {t('ssh.portForward.manualTitle')} +
-
- - - - - +
+
+ + + + + + + +
- + checked={exposeOnLan} + onChange={(event) => setExposeOnLan(event.target.checked)} + label={t('ssh.portForward.exposeOnLan')} + description={t('ssh.portForward.exposeOnLanHint')} + />
- - setExposeOnLan(event.target.checked)} - label={t('ssh.portForward.exposeOnLan')} - title={t('ssh.portForward.exposeOnLanHint')} - />
diff --git a/src/web-ui/src/features/ssh-remote/appearance.ts b/src/web-ui/src/features/ssh-remote/appearance.ts index 81b29e814c..ce0b5e5865 100644 --- a/src/web-ui/src/features/ssh-remote/appearance.ts +++ b/src/web-ui/src/features/ssh-remote/appearance.ts @@ -28,6 +28,7 @@ export const sshRemoteAppearanceDescriptor: AppearanceSurfaceDescriptor = { { id: 'confirmMessage' }, { id: 'confirmActions' }, { id: 'portForward' }, + { id: 'portForwardIntro' }, { id: 'portForwardForm' }, { id: 'portForwardError' }, { id: 'portForwardTable' }, diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 68fa8030fb..3b921e5108 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -1689,6 +1689,7 @@ "portForward": { "menuEntry": "Forward a Port", "title": "Port Forwarding", + "intro": "Reach a port listening on the remote host from this machine. Nothing is forwarded automatically — only the mappings you add here.", "detectedTitle": "Listening on the remote", "detecting": "Detecting remote ports…", "detect": "Detect again", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index ee14eee845..97a4b14b48 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -1689,6 +1689,7 @@ "portForward": { "menuEntry": "端口映射", "title": "端口映射", + "intro": "把远程主机上正在监听的端口映射到本机访问。映射不会自动建立,只有你在这里添加的才会生效。", "detectedTitle": "远程正在监听", "detecting": "正在检测远程端口…", "detect": "重新检测", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 96e631c209..9aafcce81f 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -1689,6 +1689,7 @@ "portForward": { "menuEntry": "連接埠對應", "title": "連接埠對應", + "intro": "把遠端主機上正在監聽的連接埠對應到本機存取。對應不會自動建立,只有你在這裡新增的才會生效。", "detectedTitle": "遠端正在監聽", "detecting": "正在偵測遠端連接埠…", "detect": "重新偵測", From 6bc67e56f1a699b3210607be2a751dd91dc21464 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 21 Aug 2026 00:29:50 -0700 Subject: [PATCH 3/5] fix(remote-ssh): put PortForward in the agent tool allowlists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool was registered but no Agent could see it. `GetToolSpec`'s deferred catalog is built from each mode's allowed-tool list, and `PortForward` was in none of them — so an Agent asked to expose a remote server had no idea the capability existed and answered with hand-written `ssh -L` instructions for the user to run. Add it to the modes that already carry the environment-control set. Cron is the marker rather than ExecCommand, which even the deliberately narrow modes have: `minimal` is a fixed six-tool manifest and `Ultra` is a planner whose workers do the running, and neither carries Git or Cron either. A registry test now enforces the pairing. Also close the gaps a filtered test run had been hiding: - Both tool-manifest stability snapshots needed the new name. They exist so a manifest change is deliberate; this is that deliberate change. - Add a materialization test that every planned tool name resolves. Registry construction treats an unknown name as fatal, so this is the guard for the class of break that took out the SDK host build. - Mirror `ensure_connected` into the disabled SSH stub, which had fallen out of parity with the concrete manager. - Drop five dead `column*` locale keys left over from the table layout the row layout replaced, in all three locales. - Pin the shared-registry invariant: the UI and the tool must hold one registry, or a forward made on one surface is invisible to the other. --- .../agentic/agents/definitions/modes/claw.rs | 4 ++ .../agents/definitions/modes/cowork.rs | 4 ++ .../agents/definitions/modes/deep_research.rs | 4 ++ .../agentic/agents/definitions/modes/team.rs | 4 ++ .../assembly/core/src/agentic/agents/mod.rs | 5 ++ .../core/src/agentic/agents/registry/tests.rs | 30 ++++++++++++ .../src/agentic/tools/agent-tool-exposure.md | 1 + .../tools/product_runtime/materialization.rs | 47 +++++++++++++++++++ .../core/src/agentic/tools/registry.rs | 2 + .../src/remote_ssh/disabled.rs | 4 ++ .../src/remote_ssh/port_forward.rs | 14 ++++++ src/web-ui/src/locales/en-US/common.json | 5 -- src/web-ui/src/locales/zh-CN/common.json | 5 -- src/web-ui/src/locales/zh-TW/common.json | 5 -- 14 files changed, 119 insertions(+), 15 deletions(-) diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/claw.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/claw.rs index 1a7cb1bef3..d0f8814ffe 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/claw.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/claw.rs @@ -28,6 +28,10 @@ impl ClawMode { "ExecCommand".to_string(), "WriteStdin".to_string(), "ExecControl".to_string(), + // The companion to ExecCommand for remote work: a server + // started on an SSH host is unreachable from the user's + // machine until a forward exists. + "PortForward".to_string(), "Grep".to_string(), "Glob".to_string(), "WebSearch".to_string(), diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/cowork.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/cowork.rs index 3d163af356..1fea14be23 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/cowork.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/cowork.rs @@ -53,6 +53,10 @@ impl CoworkMode { "ExecCommand".to_string(), "WriteStdin".to_string(), "ExecControl".to_string(), + // The companion to ExecCommand for remote work: a server + // started on an SSH host is unreachable from the user's + // machine until a forward exists. + "PortForward".to_string(), "WebSearch".to_string(), "WebFetch".to_string(), "ControlHub".to_string(), diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/deep_research.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/deep_research.rs index 5eababbd82..cccc7cd3cb 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/deep_research.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/deep_research.rs @@ -38,6 +38,10 @@ impl DeepResearchMode { "ExecCommand".to_string(), "WriteStdin".to_string(), "ExecControl".to_string(), + // The companion to ExecCommand for remote work: a server + // started on an SSH host is unreachable from the user's + // machine until a forward exists. + "PortForward".to_string(), "ControlHub".to_string(), // Standing research ("re-check these sources every morning") // belongs on a schedule, and ControlHub's `wait` points here diff --git a/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs b/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs index 293fa3d07a..68624a6dd1 100644 --- a/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs +++ b/src/crates/assembly/core/src/agentic/agents/definitions/modes/team.rs @@ -33,6 +33,10 @@ impl TeamMode { "ExecCommand".to_string(), "WriteStdin".to_string(), "ExecControl".to_string(), + // The companion to ExecCommand for remote work: a server + // started on an SSH host is unreachable from the user's + // machine until a forward exists. + "PortForward".to_string(), "Grep".to_string(), "Glob".to_string(), "WebSearch".to_string(), diff --git a/src/crates/assembly/core/src/agentic/agents/mod.rs b/src/crates/assembly/core/src/agentic/agents/mod.rs index 6839a347cb..6ce995ee15 100644 --- a/src/crates/assembly/core/src/agentic/agents/mod.rs +++ b/src/crates/assembly/core/src/agentic/agents/mod.rs @@ -132,6 +132,11 @@ pub fn shared_coding_mode_tools() -> Vec { "ExecCommand".to_string(), "WriteStdin".to_string(), "ExecControl".to_string(), + // The companion to ExecCommand for remote work: a server started on + // an SSH host is unreachable from the user's machine until a forward + // exists, and an Agent that cannot see this tool reinvents it with + // hand-written `ssh -L` instructions the user has to run themselves. + "PortForward".to_string(), "Grep".to_string(), "Glob".to_string(), "WebSearch".to_string(), diff --git a/src/crates/assembly/core/src/agentic/agents/registry/tests.rs b/src/crates/assembly/core/src/agentic/agents/registry/tests.rs index f5cef171a8..836a7aa526 100644 --- a/src/crates/assembly/core/src/agentic/agents/registry/tests.rs +++ b/src/crates/assembly/core/src/agentic/agents/registry/tests.rs @@ -367,6 +367,36 @@ fn every_builtin_mode_with_control_hub_can_also_schedule_with_cron() { } } +#[test] +fn builtin_modes_carrying_the_environment_tools_can_forward_a_port_they_opened() { + // The allowlist is what the deferred catalog is built from, so a tool + // missing here is invisible to the model however it is registered — the + // Agent then reinvents the feature as hand-written `ssh -L` instructions + // for the user to run. + // + // Cron is the marker for "carries the environment-control set" rather than + // ExecCommand, which even the deliberately narrow modes have: `minimal` is + // a fixed six-tool manifest and `Ultra` is a planner whose workers do the + // running, and neither carries Git or Cron either. + for spec in builtin_agent_specs() + .into_iter() + .filter(|spec| spec.category == AgentCategory::Mode) + { + let mode = (spec.factory)(); + let default_tools = mode.default_tools(); + let runs_commands = default_tools.iter().any(|tool| tool == "ExecCommand"); + let schedules = default_tools.iter().any(|tool| tool == "Cron"); + if !(runs_commands && schedules) { + continue; + } + assert!( + default_tools.iter().any(|tool| tool == "PortForward"), + "builtin mode {} can run commands but cannot forward a port it opened", + mode.id() + ); + } +} + #[test] fn agent_list_and_delete_are_exposed_only_to_swarm_planners() { for spec in builtin_agent_specs() { diff --git a/src/crates/assembly/core/src/agentic/tools/agent-tool-exposure.md b/src/crates/assembly/core/src/agentic/tools/agent-tool-exposure.md index 1c1e6fb396..20b42fa220 100644 --- a/src/crates/assembly/core/src/agentic/tools/agent-tool-exposure.md +++ b/src/crates/assembly/core/src/agentic/tools/agent-tool-exposure.md @@ -35,6 +35,7 @@ Notes: | `SessionMessage` | Deferred | None | - | | `SessionHistory` | Deferred | None | - | | `Cron` | Deferred | None | - | +| `PortForward` | Deferred | None | - | | `WebSearch` | Deferred | `DeepResearch` | Direct | | `WebFetch` | Deferred | `DeepResearch` | Direct | | `ListMCPResources` | Deferred | None | - | diff --git a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs index b99b2e0237..49bca25193 100644 --- a/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs +++ b/src/crates/assembly/core/src/agentic/tools/product_runtime/materialization.rs @@ -169,3 +169,50 @@ pub(in crate::agentic::tools) fn create_product_tool_registry_from_plan( Ok(ToolRuntimeAssembly::with_tool_decorator(tool_decorator) .create_registry_from_static_provider_entries(entries, &ProductConcreteToolFactory)?) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::agentic::tools::framework::ToolExposure; + use bitfun_tool_packs::product_tool_provider_group_plan; + + /// Every tool name the provider plan advertises must materialize. + /// + /// Registry construction treats an unknown name as a hard error, so a tool + /// that is listed but not built takes down every session in a build that + /// requests its feature group — the failure is total, not local to the tool. + #[test] + fn every_planned_tool_name_materializes() { + let factory = ProductConcreteToolFactory; + for provider in product_tool_provider_group_plan() { + for tool_name in provider.tool_names() { + let Some(group) = tool_feature_group(tool_name) else { + panic!("{tool_name} has no feature owner"); + }; + if unavailable_feature_groups(&[group]).is_empty() { + assert!( + factory.materialize_tool(tool_name).is_some(), + "{tool_name} is planned under provider {} but does not materialize", + provider.provider_id() + ); + } + } + } + } + + #[cfg(all(feature = "tools-agent-control", feature = "remote-workspace"))] + #[test] + fn port_forward_materializes_as_a_deferred_tool() { + let tool = ProductConcreteToolFactory + .materialize_tool("PortForward") + .expect("PortForward should materialize when its feature group is present"); + assert_eq!(tool.name(), "PortForward"); + // Deferred keeps it out of the default manifest; the model finds it + // through the GetToolSpec catalog, which is built from this exposure. + assert!(matches!(tool.default_exposure(), ToolExposure::Deferred)); + assert!( + !tool.short_description().trim().is_empty(), + "the catalog entry is how the model learns this tool exists" + ); + } +} diff --git a/src/crates/assembly/core/src/agentic/tools/registry.rs b/src/crates/assembly/core/src/agentic/tools/registry.rs index 66e72ff5dd..ec649c4d80 100644 --- a/src/crates/assembly/core/src/agentic/tools/registry.rs +++ b/src/crates/assembly/core/src/agentic/tools/registry.rs @@ -596,6 +596,7 @@ mod tests { "SessionMessage", "SessionHistory", "Cron", + "PortForward", "WebSearch", "WebFetch", "ListMCPResources", @@ -792,6 +793,7 @@ mod tests { "SessionMessage", "SessionHistory", "Cron", + "PortForward", "WebSearch", "WebFetch", "ListMCPResources", diff --git a/src/crates/services/services-integrations/src/remote_ssh/disabled.rs b/src/crates/services/services-integrations/src/remote_ssh/disabled.rs index 20360f8823..25fc1836c0 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/disabled.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/disabled.rs @@ -461,6 +461,10 @@ impl SSHConnectionManager { false } + pub async fn ensure_connected(&self, _connection_id: &str) -> anyhow::Result<()> { + Err(unsupported()) + } + pub async fn execute_command( &self, _connection_id: &str, diff --git a/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs b/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs index e820c60e28..13147a7255 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/port_forward.rs @@ -1039,6 +1039,20 @@ LISTEN 0 4096 127.0.0.1:4000 0.0.0.0:* users:((\"api\",pid=12,fd=7))\n"; assert!(error.to_string().contains("remote port"), "{error}"); } + #[tokio::test] + async fn the_global_registry_is_one_shared_instance() { + // The UI and the Agent tool each take their own handle. If these were + // separate registries, a forward made on one surface would be invisible + // and unstoppable from the other, and the listener would outlive any + // way to find it. + let first = global_port_forward_manager(); + let second = global_port_forward_manager(); + assert!( + Arc::ptr_eq(&first.forwards, &second.forwards), + "every caller must share one forward registry" + ); + } + #[tokio::test] async fn stopping_an_unknown_forward_succeeds() { let manager = PortForwardManager::new(); diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 3b921e5108..0fb3d6b3aa 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -1698,11 +1698,6 @@ "detectedAlreadyForwarded": "This port is already forwarded", "activeTitle": "Forwarded", "empty": "No port mappings yet. Pick a detected port above, or add one manually below.", - "columnRemotePort": "Remote port", - "columnLocalAddress": "Local address", - "columnLabel": "Label", - "columnStatus": "Status", - "columnActions": "Actions", "portMoved": "Local {{requested}} was in use; bound {{bound}} instead", "exposedOnLanBadge": "Reachable on the LAN", "statusActive": "{{active}} connection(s)", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 97a4b14b48..61c7dd0700 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -1698,11 +1698,6 @@ "detectedAlreadyForwarded": "这个端口已经映射过了", "activeTitle": "已映射", "empty": "还没有端口映射。点上面检测到的端口,或在下方手动填写。", - "columnRemotePort": "远程端口", - "columnLocalAddress": "本地地址", - "columnLabel": "备注", - "columnStatus": "状态", - "columnActions": "操作", "portMoved": "本地 {{requested}} 被占用,已改用 {{bound}}", "exposedOnLanBadge": "局域网可访问", "statusActive": "{{active}} 个连接", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 9aafcce81f..7310e5af5b 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -1698,11 +1698,6 @@ "detectedAlreadyForwarded": "這個連接埠已經對應過了", "activeTitle": "已對應", "empty": "還沒有連接埠對應。點上面偵測到的連接埠,或在下方手動填寫。", - "columnRemotePort": "遠端連接埠", - "columnLocalAddress": "本機位址", - "columnLabel": "備註", - "columnStatus": "狀態", - "columnActions": "操作", "portMoved": "本機 {{requested}} 已被占用,改用 {{bound}}", "exposedOnLanBadge": "區域網路可存取", "statusActive": "{{active}} 個連線", From af9683681fb7c7361fed6af8ede49a39b719cf56 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 21 Aug 2026 00:46:27 -0700 Subject: [PATCH 4/5] fix(web-ui): pair the appearance surface and part attributes on port forward nodes `appearance:contract-audit` requires `data-bf-component` and `data-bf-part` to be declared together on the same node, and two nodes carried only the part. The audit runs inside `build:web`, which is why a passing type-check, lint, and test run still left the Frontend Build job red. --- BitFun-Installer/src-tauri/Cargo.lock | 5535 +++++++++++++++++ .../src-tauri/gen/schemas/macOS-schema.json | 2358 +++++++ .../features/ssh-remote/PortForwardDialog.tsx | 12 +- 3 files changed, 7903 insertions(+), 2 deletions(-) create mode 100644 BitFun-Installer/src-tauri/Cargo.lock create mode 100644 BitFun-Installer/src-tauri/gen/schemas/macOS-schema.json diff --git a/BitFun-Installer/src-tauri/Cargo.lock b/BitFun-Installer/src-tauri/Cargo.lock new file mode 100644 index 0000000000..4748ef57d7 --- /dev/null +++ b/BitFun-Installer/src-tauri/Cargo.lock @@ -0,0 +1,5535 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitfun-agent-stream" +version = "0.2.18" +dependencies = [ + "anyhow", + "async-trait", + "bitfun-core-types", + "bitfun-events", + "bitfun-tool-call-jsonrepair", + "futures", + "log", + "serde", + "serde_json", + "tokio", + "tokio-util", + "uuid", +] + +[[package]] +name = "bitfun-ai-adapters" +version = "0.2.18" +dependencies = [ + "anyhow", + "async-trait", + "bitfun-agent-stream", + "bitfun-core-types", + "chrono", + "eventsource-stream", + "futures", + "hex", + "log", + "reqwest", + "serde", + "serde_json", + "sha2", + "tokio", + "tokio-stream", + "tokio-util", + "urlencoding", +] + +[[package]] +name = "bitfun-core-types" +version = "0.2.18" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "bitfun-events" +version = "0.2.18" +dependencies = [ + "anyhow", + "async-trait", + "bitfun-core-types", + "chrono", + "log", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "bitfun-installer" +version = "0.2.18" +dependencies = [ + "anyhow", + "bitfun-ai-adapters", + "chrono", + "dirs 5.0.1", + "log", + "mslnk", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "winreg 0.52.0", + "zip", +] + +[[package]] +name = "bitfun-tool-call-jsonrepair" +version = "0.2.18" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "h2" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mslnk" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c97310150b7f496a93f31690da7822b99d95ff68ca9d30fb09d3ad54375c76" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "log", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.20", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/BitFun-Installer/src-tauri/gen/schemas/macOS-schema.json b/BitFun-Installer/src-tauri/gen/schemas/macOS-schema.json new file mode 100644 index 0000000000..24c90014ed --- /dev/null +++ b/BitFun-Installer/src-tauri/gen/schemas/macOS-schema.json @@ -0,0 +1,2358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx index ac044345c8..b8723ffcbf 100644 --- a/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx +++ b/src/web-ui/src/features/ssh-remote/PortForwardDialog.tsx @@ -238,7 +238,11 @@ export const PortForwardDialog: React.FC = ({ data-bf-component="ssh-remote" data-bf-part="portForward" > -
+
@@ -319,7 +323,11 @@ export const PortForwardDialog: React.FC = ({
-
+
{t('ssh.portForward.activeTitle')}
From 8f306da1bf0d04fbdbcabcfe40e77d8f380192eb Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Fri, 21 Aug 2026 01:35:27 -0700 Subject: [PATCH 5/5] fix(desktop): declare the remote workspace policy for the port forward commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `every_registered_command_declares_a_remote_workspace_policy` requires every command in `generate_handler!` to state how it behaves for a remote workspace, and the four port-forwarding commands stated nothing. They are `WorkspaceAgnostic`, matching every other `ssh_*` command: each takes an explicit connection or forward id and does not depend on where the workspace filesystem lives. This is a `bitfun-desktop --lib` test, which is the suite a pre-existing `format_args!` borrow error in `logging.rs` prevents from compiling on newer stable toolchains — so it never ran locally. Verified by patching that error out locally: core 2070 passed, desktop 323 passed. --- .../desktop/src/api/remote_workspace_policy.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/apps/desktop/src/api/remote_workspace_policy.rs b/src/apps/desktop/src/api/remote_workspace_policy.rs index 11f1b1e5f9..e36545181d 100644 --- a/src/apps/desktop/src/api/remote_workspace_policy.rs +++ b/src/apps/desktop/src/api/remote_workspace_policy.rs @@ -1884,6 +1884,14 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] = "ssh_list_docker_containers", RemoteWorkspacePolicy::WorkspaceAgnostic, ), + ( + "ssh_list_port_forwards", + RemoteWorkspacePolicy::WorkspaceAgnostic, + ), + ( + "ssh_list_remote_listening_ports", + RemoteWorkspacePolicy::WorkspaceAgnostic, + ), ( "ssh_list_saved_connections", RemoteWorkspacePolicy::WorkspaceAgnostic, @@ -1892,6 +1900,14 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] = "ssh_save_connection", RemoteWorkspacePolicy::WorkspaceAgnostic, ), + ( + "ssh_start_port_forward", + RemoteWorkspacePolicy::WorkspaceAgnostic, + ), + ( + "ssh_stop_port_forward", + RemoteWorkspacePolicy::WorkspaceAgnostic, + ), ( "ssh_test_connection", RemoteWorkspacePolicy::WorkspaceAgnostic,