@@ -52,6 +52,66 @@ function Test-ConfigObject {
5252 return $false
5353}
5454
55+ function Resolve-ContextPath {
56+ param (
57+ [Parameter (Mandatory = $true )][string ]$Root ,
58+ [Parameter (Mandatory = $true )][string ]$RelativePath
59+ )
60+
61+ $rootFull = [System.IO.Path ]::GetFullPath($Root )
62+ $segments = $RelativePath -split ' /'
63+ $resolved = $rootFull
64+
65+ foreach ($segment in $segments ) {
66+ if ([string ]::IsNullOrWhiteSpace($segment ) -or $segment -eq ' .' ) {
67+ continue
68+ }
69+
70+ $candidate = [System.IO.Path ]::GetFullPath((Join-Path $resolved $segment ))
71+ if (Test-Path - LiteralPath $candidate ) {
72+ $item = Get-Item - LiteralPath $candidate - Force
73+ if ($item.Attributes -band [System.IO.FileAttributes ]::ReparsePoint) {
74+ $target = $item.Target
75+ if ($target -is [System.Array ]) {
76+ $target = $target [0 ]
77+ }
78+ if ($target ) {
79+ if ([System.IO.Path ]::IsPathRooted($target )) {
80+ $candidate = [System.IO.Path ]::GetFullPath($target )
81+ } else {
82+ $candidate = [System.IO.Path ]::GetFullPath(
83+ (Join-Path (Split-Path - Parent $candidate ) $target )
84+ )
85+ }
86+ }
87+ }
88+ }
89+ $resolved = $candidate
90+ }
91+
92+ return $resolved
93+ }
94+
95+ function Test-IsSubPath {
96+ param (
97+ [Parameter (Mandatory = $true )][string ]$Root ,
98+ [Parameter (Mandatory = $true )][string ]$Path
99+ )
100+
101+ $comparison = if ([System.Environment ]::OSVersion.Platform -eq [System.PlatformID ]::Win32NT) {
102+ [System.StringComparison ]::OrdinalIgnoreCase
103+ } else {
104+ [System.StringComparison ]::Ordinal
105+ }
106+ $rootFull = [System.IO.Path ]::GetFullPath($Root ).TrimEnd(
107+ [System.IO.Path ]::DirectorySeparatorChar,
108+ [System.IO.Path ]::AltDirectorySeparatorChar
109+ )
110+ $pathFull = [System.IO.Path ]::GetFullPath($Path )
111+ return $pathFull.Equals ($rootFull , $comparison ) -or
112+ $pathFull.StartsWith ($rootFull + [System.IO.Path ]::DirectorySeparatorChar, $comparison )
113+ }
114+
55115$ErrorActionPreference = ' Stop'
56116$DefaultStart = ' <!-- SPECKIT START -->'
57117$DefaultEnd = ' <!-- SPECKIT END -->'
@@ -143,8 +203,8 @@ if (-not (Test-ConfigObject -Object $Options)) {
143203
144204$ConfiguredContextFiles = Get-ConfigValue - Object $Options - Key ' context_files'
145205$ContextFiles = @ ()
146- if ($ConfiguredContextFiles -is [ System.Array ] ) {
147- foreach ($item in $ConfiguredContextFiles ) {
206+ if ($null -ne $ConfiguredContextFiles ) {
207+ foreach ($item in @ ( $ConfiguredContextFiles ) ) {
148208 if ($item -is [string ] -and -not [string ]::IsNullOrWhiteSpace($item )) {
149209 $ContextFiles += $item.Trim ()
150210 }
@@ -163,16 +223,25 @@ if ($ContextFiles.Count -eq 0) {
163223}
164224
165225foreach ($ContextFile in $ContextFiles ) {
166- # Reject absolute paths and '..' path segments in context files
226+ # Reject absolute paths, backslash separators, and '..' path segments in context files
167227 if ([System.IO.Path ]::IsPathRooted($ContextFile )) {
168228 Write-Warning " agent-context: context files must be project-relative paths; got '$ContextFile '."
169229 exit 1
170230 }
231+ if ($ContextFile.Contains (' \' )) {
232+ Write-Warning " agent-context: context files must not contain backslash separators; got '$ContextFile '."
233+ exit 1
234+ }
171235 $cfSegments = $ContextFile -split ' [/\\]'
172236 if ($cfSegments -contains ' ..' ) {
173237 Write-Warning " agent-context: context files must not contain '..' path segments; got '$ContextFile '."
174238 exit 1
175239 }
240+ $resolvedTarget = Resolve-ContextPath - Root $ProjectRoot - RelativePath $ContextFile
241+ if (-not (Test-IsSubPath - Root $ProjectRoot - Path $resolvedTarget )) {
242+ Write-Warning " agent-context: context file path resolves outside the project root; got '$ContextFile '."
243+ exit 1
244+ }
176245}
177246
178247$MarkerStart = $DefaultStart
0 commit comments