22//!
33//! Functions for validating filesystem access and enforcing workspace trust.
44
5- use std:: path:: Path ;
5+ use std:: path:: { Path , PathBuf } ;
66
77use CommonLibrary :: Error :: CommonError :: CommonError ;
88
@@ -11,13 +11,35 @@ use crate::{ApplicationState::ApplicationState, dev_log};
1111/// A critical security helper that checks if a given filesystem path is
1212/// allowed for access.
1313///
14- /// In this architecture, this means the path must be a descendant of one of the
15- /// currently open and trusted workspace folders. This prevents extensions from
16- /// performing arbitrary filesystem operations outside the user's intended
17- /// scope.
14+ /// The access model has two tiers:
15+ ///
16+ /// 1. **Trusted system paths** - directories Land itself owns (user
17+ /// extensions, agent plugins, app-support storage, bundled extension
18+ /// roots). These are never "user content" and the extension scanner,
19+ /// VSIX installer, and global-storage probes must be able to read/write
20+ /// them regardless of which workspace folder is open. They bypass the
21+ /// workspace-folder check entirely.
22+ ///
23+ /// 2. **Workspace content** - everything else is only reachable when the
24+ /// resolved path is a descendant of a currently registered, trusted
25+ /// workspace folder. That's the sandbox boundary that keeps extensions
26+ /// from rifling through `$HOME` via `vscode.workspace.fs`.
27+ ///
28+ /// Without tier 1, the scanner's read of `~/.land/extensions` is
29+ /// rejected as "Path is outside of the registered workspace folders", so
30+ /// user-installed VSIXes never reach the Extensions sidebar even though
31+ /// they are present on disk.
1832pub fn IsPathAllowedForAccess ( ApplicationState : & ApplicationState , PathToCheck : & Path ) -> Result < ( ) , CommonError > {
1933 dev_log ! ( "vfs" , "[EnvironmentSecurity] Verifying path: {}" , PathToCheck . display( ) ) ;
2034
35+ // Tier 1: trusted system paths bypass workspace gating. See
36+ // `IsTrustedSystemPath` for the complete allow-list. Scanner reads,
37+ // VSIX installs, agent-plugin probes, and per-extension global-storage
38+ // stats hit this path on every boot.
39+ if IsTrustedSystemPath ( PathToCheck ) {
40+ return Ok ( ( ) ) ;
41+ }
42+
2143 if !ApplicationState . Workspace . IsTrusted . load ( std:: sync:: atomic:: Ordering :: Relaxed ) {
2244 return Err ( CommonError :: FileSystemPermissionDenied {
2345 Path : PathToCheck . to_path_buf ( ) ,
@@ -53,3 +75,136 @@ pub fn IsPathAllowedForAccess(ApplicationState:&ApplicationState, PathToCheck:&P
5375 } )
5476 }
5577}
78+
79+ /// Return `true` when `PathToCheck` falls under a directory that Land itself
80+ /// manages and the sandbox should not gate.
81+ ///
82+ /// Covered roots:
83+ ///
84+ /// - `${LAND_USER_EXTENSION_DIRECTORY}` (explicit override, if set).
85+ /// - `$HOME/.land/**` - the canonical namespace for user-installed
86+ /// extensions, agent plugins, global storage, and any other Land-owned
87+ /// state that lives outside the VS Code-style profile tree.
88+ /// - The Mountain executable's own `extensions/`, `../Resources/extensions/`
89+ /// and `../Resources/app/extensions/` neighbours - built-in extension
90+ /// roots that ship inside the `.app` bundle.
91+ /// - `$APPDATA`-equivalents: Tauri's resolved app-data / app-config /
92+ /// app-local directories (via `$XDG_DATA_HOME`, `$XDG_CONFIG_HOME` if
93+ /// set; on macOS the `Library/Application Support/land.editor.*` tree).
94+ /// - `${TMPDIR}` - short-lived temp files the installer unpacks into.
95+ ///
96+ /// Anything outside this list still flows through the workspace-folder
97+ /// check. The set is intentionally narrow: it unblocks Land's *own*
98+ /// bookkeeping reads without handing extensions an unbounded filesystem.
99+ fn IsTrustedSystemPath ( PathToCheck : & Path ) -> bool {
100+ // Canonicalising is best-effort - when the path doesn't exist yet
101+ // (e.g. first-boot probes for `globalStorage/<extension>/state.json`)
102+ // `canonicalize` returns Err and we compare against the raw path.
103+ let Candidate = PathToCheck . canonicalize ( ) . unwrap_or_else ( |_| PathToCheck . to_path_buf ( ) ) ;
104+
105+ if let Ok ( Override ) = std:: env:: var ( "LAND_USER_EXTENSION_DIRECTORY" ) {
106+ if !Override . is_empty ( ) {
107+ let OverridePath = PathBuf :: from ( & Override ) ;
108+ if Candidate . starts_with ( & OverridePath ) || PathToCheck . starts_with ( & OverridePath ) {
109+ return true ;
110+ }
111+ }
112+ }
113+
114+ if let Ok ( Home ) = std:: env:: var ( "HOME" ) {
115+ let LandRoot = PathBuf :: from ( & Home ) . join ( ".land" ) ;
116+ if Candidate . starts_with ( & LandRoot ) || PathToCheck . starts_with ( & LandRoot ) {
117+ return true ;
118+ }
119+
120+ // macOS / Linux Application-Support trees that host Land's per-profile
121+ // state. `land.editor.*` prefix matches every build profile variant.
122+ let MacAppSupport = PathBuf :: from ( & Home ) . join ( "Library/Application Support" ) ;
123+ if ( Candidate . starts_with ( & MacAppSupport ) || PathToCheck . starts_with ( & MacAppSupport ) )
124+ && ContainsLandEditorSegment ( PathToCheck )
125+ {
126+ return true ;
127+ }
128+
129+ let XdgConfig = std:: env:: var ( "XDG_CONFIG_HOME" ) . map ( PathBuf :: from) . unwrap_or_else ( |_| PathBuf :: from ( & Home ) . join ( ".config" ) ) ;
130+ if ( Candidate . starts_with ( & XdgConfig ) || PathToCheck . starts_with ( & XdgConfig ) )
131+ && ContainsLandEditorSegment ( PathToCheck )
132+ {
133+ return true ;
134+ }
135+
136+ let XdgData = std:: env:: var ( "XDG_DATA_HOME" ) . map ( PathBuf :: from) . unwrap_or_else ( |_| PathBuf :: from ( & Home ) . join ( ".local/share" ) ) ;
137+ if ( Candidate . starts_with ( & XdgData ) || PathToCheck . starts_with ( & XdgData ) )
138+ && ContainsLandEditorSegment ( PathToCheck )
139+ {
140+ return true ;
141+ }
142+ }
143+
144+ if let Ok ( Exe ) = std:: env:: current_exe ( ) {
145+ if let Some ( ExeParent ) = Exe . parent ( ) {
146+ let BundleRoots = [
147+ ExeParent . join ( "extensions" ) ,
148+ ExeParent . join ( "../Resources/extensions" ) ,
149+ ExeParent . join ( "../Resources/app/extensions" ) ,
150+ // Sky's Static/Application/extensions root is reached via
151+ // `../../../Sky/Target/Static/Application/extensions` in the
152+ // debug profile - match the canonical `Sky/Target/Static/Application/extensions`
153+ // segment regardless of how many `..` hops the scan path used.
154+ ] ;
155+ for Root in BundleRoots {
156+ let Normalised = Root . canonicalize ( ) . unwrap_or ( Root . clone ( ) ) ;
157+ if Candidate . starts_with ( & Normalised ) || PathToCheck . starts_with ( & Root ) {
158+ return true ;
159+ }
160+ }
161+ }
162+ }
163+
164+ // Sky / Dependency bundled extension trees. These are debug-profile
165+ // layouts where the scanner reaches the bundle root via relative hops
166+ // from the Mountain executable directory - canonicalising already
167+ // resolves that, but we also fall back to a path-segment match so a
168+ // missing file (first-boot probe) still clears the check.
169+ if ContainsPathSegments ( PathToCheck , & [ "Sky" , "Target" , "Static" , "Application" , "extensions" ] )
170+ || ContainsPathSegments ( PathToCheck , & [ "Dependency" , "Microsoft" , "Dependency" , "Editor" , "extensions" ] )
171+ {
172+ return true ;
173+ }
174+
175+ if let Ok ( TempDir ) = std:: env:: var ( "TMPDIR" ) {
176+ let TempPath = PathBuf :: from ( & TempDir ) ;
177+ if !TempPath . as_os_str ( ) . is_empty ( )
178+ && ( Candidate . starts_with ( & TempPath ) || PathToCheck . starts_with ( & TempPath ) )
179+ {
180+ return true ;
181+ }
182+ }
183+
184+ false
185+ }
186+
187+ /// True when `path` contains a directory segment whose name starts with
188+ /// `land.editor.`. Used to tighten the Application-Support / XDG checks so
189+ /// we only trust directories that Land itself provisioned, not every file
190+ /// under `$HOME/Library/Application Support`.
191+ fn ContainsLandEditorSegment ( path : & Path ) -> bool {
192+ path. components ( ) . any ( |Component | {
193+ Component
194+ . as_os_str ( )
195+ . to_str ( )
196+ . map ( |Name | Name . starts_with ( "land.editor." ) )
197+ . unwrap_or ( false )
198+ } )
199+ }
200+
201+ /// True when every element of `segments` appears in order as consecutive
202+ /// path components of `path`. Used to match Sky / Dependency extension
203+ /// roots regardless of which relative-path prefix the scanner used.
204+ fn ContainsPathSegments ( path : & Path , segments : & [ & str ] ) -> bool {
205+ let Names : Vec < & str > = path. components ( ) . filter_map ( |C | C . as_os_str ( ) . to_str ( ) ) . collect ( ) ;
206+ if segments. is_empty ( ) || Names . len ( ) < segments. len ( ) {
207+ return false ;
208+ }
209+ Names . windows ( segments. len ( ) ) . any ( |Window | Window . iter ( ) . zip ( segments. iter ( ) ) . all ( |( A , B ) | A == B ) )
210+ }
0 commit comments