Run Python smoke code in AppContainer
This commit is contained in:
@@ -99,6 +99,8 @@
|
||||
4. [x] كتابة مضبوطة: إنشاء وتعديل ملفات داخل مساحة العمل فقط، مع معاينة diff وتأكيد المستخدم قبل التطبيق. (2026-10-02: لا كتابة عند المعاينة؛ الرمز مؤقت ولمرة واحدة، وفحص المسار والبصمة يعاد قبل التطبيق؛ اجتازت اختبارات Python واختبارات واجهة Flutter، وأُعيد تشغيل Windows Debug وFastAPI بالتغييرات.)
|
||||
5. أوامر تطوير: تشغيل أوامر محددة في بيئة معزولة وبمهلة وحدود موارد، ومع موافقة لكل أمر في البداية. (2026-10-03: تحقق Windows 10 Pro 19045، Intel i7-6600U مع virtualization firmware مفعّل، RAM 15.9GB والمتاح وقت القياس 5.2GB، و40.8GB مساحة فارغة على C:. لا يوجد `WindowsSandbox.exe` أو Docker. استعلاما WSL أعادا شاشة المساعدة فلم يثبتا توفر توزيعة. فحص Windows Sandbox يحتاج مسؤولًا؛ محاولة DISM مرتفعة الصلاحية انتهت بخطأ `0xc0000142` ولم تغيّر إعدادًا. أُعدّ prototype محلي بـAppContainer وJob Object (`scripts/appcontainer_probe.cpp`): 512 MiB، حد 8 عمليات، مهلة 30 ثانية، وإنهاء شجرة العمليات عند إغلاق الـJob. في تشغيل Windows بأذونات مناسبة نجح smoke test: `cmd.exe` عمل داخل الحاوية؛ مُنع من قراءة ملف Temp للمضيف ومن إنشاء ملف خارجه، بينما نجح في الكتابة والقراءة من مجلد العمل المعزول. نُسخ `README.md` من المشروع إلى الحاوية ثم استخدم `curl.exe file://` لنسخه منها؛ النسخة تطابقت بايتًا ببايت. اختُبر `curl.exe` داخل الحاوية (`--version` exit 0)، وفشل الوصول إلى `/health` على `127.0.0.1:8100` بمهلة curl 28 رغم نجاح endpoint من المضيف؛ هذا فحص اتصال محلي فقط، وليس اختبارًا للإنترنت العام. إعداد AppContainer بلا قدرات شبكية. الجلسة المقيدة لدى Codex فشلت في إنشاء الملف الشخصي بـ`0x80070005`، بينما نجح الفاحص عبر جلسة التنفيذ المسموحة؛ يحتوي `scripts/run_appcontainer_probe.ps1` على build وتشغيل وتنظيف مؤقت قابل للتكرار. ما زال هذا prototype غير مدمج في الوكيل ولا توجد أوامر عامة قابلة للتنفيذ. التالي: تجربة نسخ ملفات محددة وآمنة من مساحة يختارها المستخدم مع حدود حجم واستثناء الأسرار والروابط الرمزية، ثم إرجاع المخرجات/diff والتحقق من الموارد والمهلة، وبعدها دمج قائمة أوامر مسموحة وموافقة صريحة في API والواجهة. [AppContainer isolation](https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation)، [تنفيذ AppContainer](https://learn.microsoft.com/en-us/windows/win32/secauthz/implementing-an-appcontainer)، [Job Objects](https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects).)
|
||||
- [x] تجهيز Snapshot محدود لملفات يختارها المستخدم (`app/execution_snapshot.py`): يفرض جذر workspace المعتمد للحساب، حتى 50 ملفًا، 512KB لكل ملف و10MB إجماليًا، ويقبل الامتدادات المدعومة فقط. يرفض المسارات المخفية/المستثناة/الخارجة، والروابط الرمزية، وأسماء أجهزة Windows المحجوزة، وأسماء الملفات/المحتوى التي تكشف مفاتيح معروفة أو قيم اعتماد مباشرة؛ وينسخ إلى مجلد مؤقت مع SHA-256 لكل ملف. 7 اختبارات snapshot و7 اختبارات workspace ناجحة (2026-10-03). فحص الأسرار محافظ وليس ماسحًا شاملًا ولا يغني عن المراجعة. هذا staging host-side مثبت وحده ولم يُوصل بعد إلى AppContainer أو API؛ حدّ اكتماله هو تجهيز النسخة فقط.
|
||||
- [x] اختبار runtime Python داخل AppContainer: نُسخت ملفات التشغيل القياسية وDLLs اللازمة من Python 3.14 إلى المساحة المؤقتة (33,627,970 بايت/631 ملفًا، دون `site-packages`)، ثم شغّل Python ملف smoke ثابتًا وكتب رقم الإصدار داخل المساحة المعزولة (`python_run_exit=0`). هذا يثبت تشغيل runtime فقط؛ لا يشغّل ملفًا اختاره المستخدم، ولا يلتقط stdout/stderr، ولم يثبت حدود حجم الملفات التي يمكن أن يولدها الأمر.
|
||||
- [ ] ربط snapshot الفعلي بالمشغل الأصلي: نقل قائمة الملفات المنقاة من `execution_snapshot` إلى AppContainer، التحقق منها في broker، تشغيل ملف Python محدد أو أمر allowlist، التقاط stdout/stderr بحد صريح، فرض حد لمساحة القرص والمهلة والذاكرة، ثم إرجاع النتيجة ومعاينة التغييرات وطلب الموافقة قبل تطبيقها عبر API والواجهة.
|
||||
6. لا وصول عام إلى القرص، ولا أوامر مدمرة أو نشر خارجي دون موافقة صريحة. كل أداة لها مخطط مدخلات ومخرجات واختبارات وسجل تدقيق.
|
||||
|
||||
### كودكس للبرمجة
|
||||
|
||||
@@ -154,9 +154,126 @@ static bool FilesMatch(const std::wstring& leftPath, const std::wstring& rightPa
|
||||
return matches;
|
||||
}
|
||||
|
||||
static DWORD RunContained(
|
||||
static constexpr ULONGLONG kMaxPythonRuntimeBytes = 100ull * 1024 * 1024;
|
||||
static constexpr DWORD kMaxPythonRuntimeFiles = 5000;
|
||||
|
||||
static bool CopyRuntimeFile(
|
||||
const std::wstring& source, const std::wstring& destination,
|
||||
ULONGLONG& copiedBytes, DWORD& copiedFiles
|
||||
) {
|
||||
WIN32_FILE_ATTRIBUTE_DATA attributes{};
|
||||
if (!GetFileAttributesExW(source.c_str(), GetFileExInfoStandard, &attributes) ||
|
||||
(attributes.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) {
|
||||
return false;
|
||||
}
|
||||
const ULONGLONG size = (static_cast<ULONGLONG>(attributes.nFileSizeHigh) << 32) |
|
||||
attributes.nFileSizeLow;
|
||||
if (copiedFiles >= kMaxPythonRuntimeFiles || size > kMaxPythonRuntimeBytes - copiedBytes) {
|
||||
return false;
|
||||
}
|
||||
if (!CopyFileW(source.c_str(), destination.c_str(), FALSE)) return false;
|
||||
copiedBytes += size;
|
||||
++copiedFiles;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CopyRuntimeTree(
|
||||
const std::wstring& source, const std::wstring& destination,
|
||||
ULONGLONG& copiedBytes, DWORD& copiedFiles, unsigned depth = 0
|
||||
) {
|
||||
if (depth > 24 || copiedFiles > kMaxPythonRuntimeFiles) return false;
|
||||
if (!CreateDirectoryW(destination.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
||||
return false;
|
||||
}
|
||||
WIN32_FIND_DATAW entry{};
|
||||
HANDLE search = FindFirstFileW((source + L"\\*").c_str(), &entry);
|
||||
if (search == INVALID_HANDLE_VALUE) return false;
|
||||
bool success = true;
|
||||
do {
|
||||
if (wcscmp(entry.cFileName, L".") == 0 || wcscmp(entry.cFileName, L"..") == 0) continue;
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) continue;
|
||||
const std::wstring sourcePath = source + L"\\" + entry.cFileName;
|
||||
const std::wstring destinationPath = destination + L"\\" + entry.cFileName;
|
||||
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||
if (wcscmp(entry.cFileName, L"site-packages") == 0 ||
|
||||
wcscmp(entry.cFileName, L"__pycache__") == 0 ||
|
||||
wcscmp(entry.cFileName, L"test") == 0 ||
|
||||
wcscmp(entry.cFileName, L"tests") == 0 ||
|
||||
wcscmp(entry.cFileName, L"idlelib") == 0 ||
|
||||
wcscmp(entry.cFileName, L"tkinter") == 0 ||
|
||||
wcscmp(entry.cFileName, L"ensurepip") == 0) continue;
|
||||
success = CopyRuntimeTree(sourcePath, destinationPath, copiedBytes,
|
||||
copiedFiles, depth + 1);
|
||||
} else {
|
||||
success = CopyRuntimeFile(sourcePath, destinationPath, copiedBytes, copiedFiles);
|
||||
}
|
||||
if (!success) break;
|
||||
} while (FindNextFileW(search, &entry));
|
||||
const DWORD finalError = GetLastError();
|
||||
FindClose(search);
|
||||
return success && finalError == ERROR_NO_MORE_FILES;
|
||||
}
|
||||
|
||||
static bool CopyPythonRuntime(
|
||||
const std::wstring& sourceRoot, const std::wstring& destinationRoot,
|
||||
ULONGLONG& copiedBytes, DWORD& copiedFiles
|
||||
) {
|
||||
copiedBytes = 0;
|
||||
copiedFiles = 0;
|
||||
if (!CreateDirectoryW(destinationRoot.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
||||
return false;
|
||||
}
|
||||
static const wchar_t* const runtimeFiles[] = {
|
||||
L"python.exe", L"python3.dll", L"python314.dll",
|
||||
L"vcruntime140.dll", L"vcruntime140_1.dll"
|
||||
};
|
||||
for (const wchar_t* name : runtimeFiles) {
|
||||
const std::wstring source = sourceRoot + L"\\" + name;
|
||||
const std::wstring destination = destinationRoot + L"\\" + name;
|
||||
WIN32_FILE_ATTRIBUTE_DATA attributes{};
|
||||
if (!GetFileAttributesExW(source.c_str(), GetFileExInfoStandard, &attributes)) {
|
||||
if (wcscmp(name, L"vcruntime140_1.dll") == 0) continue;
|
||||
return false;
|
||||
}
|
||||
if (!CopyRuntimeFile(source, destination, copiedBytes, copiedFiles)) return false;
|
||||
}
|
||||
return CopyRuntimeTree(sourceRoot + L"\\Lib", destinationRoot + L"\\Lib",
|
||||
copiedBytes, copiedFiles) &&
|
||||
CopyRuntimeTree(sourceRoot + L"\\DLLs", destinationRoot + L"\\DLLs",
|
||||
copiedBytes, copiedFiles);
|
||||
}
|
||||
|
||||
static bool WriteFileBytes(const std::wstring& path, const char* bytes, DWORD length) {
|
||||
HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return false;
|
||||
DWORD written = 0;
|
||||
bool success = WriteFile(file, bytes, length, &written, nullptr) && written == length;
|
||||
CloseHandle(file);
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool IsVersionText(const std::wstring& path) {
|
||||
HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return false;
|
||||
char contents[64]{};
|
||||
DWORD count = 0;
|
||||
bool valid = ReadFile(file, contents, sizeof(contents) - 1, &count, nullptr) &&
|
||||
count >= 5 && count < sizeof(contents);
|
||||
CloseHandle(file);
|
||||
bool sawDot = false;
|
||||
for (DWORD index = 0; valid && index < count; ++index) {
|
||||
if (contents[index] == '.') sawDot = true;
|
||||
else if (contents[index] < '0' || contents[index] > '9') valid = false;
|
||||
}
|
||||
return valid && sawDot;
|
||||
}
|
||||
|
||||
static DWORD RunContainedExe(
|
||||
PSID appContainerSid,
|
||||
const std::wstring& command,
|
||||
const std::wstring& application,
|
||||
const std::wstring& arguments,
|
||||
const wchar_t* environment,
|
||||
const std::wstring& cwd,
|
||||
HANDLE job
|
||||
@@ -200,14 +317,14 @@ static DWORD RunContained(
|
||||
startup.StartupInfo.wShowWindow = SW_HIDE;
|
||||
startup.lpAttributeList = attributes;
|
||||
PROCESS_INFORMATION process{};
|
||||
std::wstring commandLine = QuoteArg(L"C:\\Windows\\System32\\cmd.exe") +
|
||||
L" /d /s /c \"" + command + L"\"";
|
||||
std::wstring commandLine = QuoteArg(application);
|
||||
if (!arguments.empty()) commandLine += L" " + arguments;
|
||||
std::wstring mutableLine = commandLine;
|
||||
constexpr DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT |
|
||||
CREATE_NO_WINDOW | CREATE_SUSPENDED;
|
||||
|
||||
BOOL created = CreateProcessW(
|
||||
L"C:\\Windows\\System32\\cmd.exe",
|
||||
application.c_str(),
|
||||
mutableLine.empty() ? nullptr : &mutableLine[0],
|
||||
nullptr,
|
||||
nullptr,
|
||||
@@ -243,6 +360,18 @@ static DWORD RunContained(
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
static DWORD RunContained(
|
||||
PSID appContainerSid,
|
||||
const std::wstring& command,
|
||||
const wchar_t* environment,
|
||||
const std::wstring& cwd,
|
||||
HANDLE job
|
||||
) {
|
||||
const std::wstring shell = L"C:\\Windows\\System32\\cmd.exe";
|
||||
return RunContainedExe(appContainerSid, shell, L"/d /s /c \"" + command + L"\"",
|
||||
environment, cwd, job);
|
||||
}
|
||||
|
||||
int wmain() {
|
||||
wchar_t tempPath[MAX_PATH]{};
|
||||
if (!GetTempPathW(MAX_PATH, tempPath)) return 10;
|
||||
@@ -307,6 +436,9 @@ int wmain() {
|
||||
std::wstring sandboxWorkspacePath = appDataPath + L"\\agent-workspace";
|
||||
std::wstring stagedInputPath = sandboxWorkspacePath + L"\\README.md";
|
||||
std::wstring stagedCopyPath = sandboxWorkspacePath + L"\\staged-copy.md";
|
||||
std::wstring pythonSandboxPath = sandboxWorkspacePath + L"\\python";
|
||||
std::wstring pythonProbePath = sandboxWorkspacePath + L"\\python_probe.py";
|
||||
std::wstring pythonVersionPath = sandboxWorkspacePath + L"\\python-version.txt";
|
||||
CreateDirectoryW(appTempPath.c_str(), nullptr);
|
||||
bool workspaceFolderReady = CreateDirectoryW(sandboxWorkspacePath.c_str(), nullptr) != FALSE ||
|
||||
GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
@@ -317,11 +449,36 @@ int wmain() {
|
||||
std::wstring curlPath = sandboxWorkspacePath + L"\\curl.exe";
|
||||
bool curlCopied = workspaceFolderReady &&
|
||||
CopyFileW(L"C:\\Windows\\System32\\curl.exe", curlPath.c_str(), FALSE);
|
||||
wchar_t pythonRootBuffer[32768]{};
|
||||
DWORD pythonRootLength = GetEnvironmentVariableW(
|
||||
L"SOVEREIGNAI_PYTHON_HOME", pythonRootBuffer, 32768);
|
||||
std::wstring pythonSourcePath = pythonRootLength > 0 && pythonRootLength < 32768
|
||||
? std::wstring(pythonRootBuffer, pythonRootLength) : std::wstring();
|
||||
ULONGLONG pythonRuntimeBytes = 0;
|
||||
DWORD pythonRuntimeFiles = 0;
|
||||
bool pythonRuntimeCopied = workspaceFolderReady && !pythonSourcePath.empty() &&
|
||||
CopyPythonRuntime(pythonSourcePath, pythonSandboxPath,
|
||||
pythonRuntimeBytes, pythonRuntimeFiles);
|
||||
const char pythonSource[] =
|
||||
"import pathlib, sys\n"
|
||||
"pathlib.Path(__file__).with_name('python-version.txt').write_text(\n"
|
||||
" '.'.join(map(str, sys.version_info[:3])), encoding='ascii')\n";
|
||||
bool pythonProbeWritten = pythonRuntimeCopied &&
|
||||
WriteFileBytes(pythonProbePath, pythonSource, sizeof(pythonSource) - 1);
|
||||
SetEnvironmentValue(environment, L"PATH", L"C:\\Windows\\System32");
|
||||
SetEnvironmentValue(environment, L"APPDATA", appDataPath);
|
||||
SetEnvironmentValue(environment, L"LOCALAPPDATA", appDataPath);
|
||||
SetEnvironmentValue(environment, L"TEMP", appTempPath);
|
||||
SetEnvironmentValue(environment, L"TMP", appTempPath);
|
||||
SetEnvironmentValue(environment, L"USERPROFILE", appDataPath);
|
||||
SetEnvironmentValue(environment, L"HOMEDRIVE", appDataPath.substr(0, 2));
|
||||
SetEnvironmentValue(environment, L"HOMEPATH", appDataPath.size() > 2
|
||||
? appDataPath.substr(2) : L"\\");
|
||||
SetEnvironmentValue(environment, L"PATH", pythonSandboxPath + L"\\DLLs;" +
|
||||
pythonSandboxPath + L";C:\\Windows\\System32");
|
||||
SetEnvironmentValue(environment, L"PYTHONHOME", pythonSandboxPath);
|
||||
SetEnvironmentValue(environment, L"PYTHONNOUSERSITE", L"1");
|
||||
SetEnvironmentValue(environment, L"PYTHONDONTWRITEBYTECODE", L"1");
|
||||
std::wstring cwd = L"C:\\Windows\\System32";
|
||||
DWORD shellResult = jobReady
|
||||
? RunContained(appContainerSid, L"exit 0", environment.data(), cwd, job)
|
||||
@@ -347,6 +504,11 @@ int wmain() {
|
||||
? RunContained(appContainerSid, QuoteArg(curlPath) + L" --fail --silent " +
|
||||
stagedFileUrl + L" -o " + stagedCopyPath, environment.data(), cwd, job)
|
||||
: ERROR_INVALID_HANDLE;
|
||||
std::wstring pythonExecutable = pythonSandboxPath + L"\\python.exe";
|
||||
DWORD pythonRunResult = jobReady && pythonProbeWritten
|
||||
? RunContainedExe(appContainerSid, pythonExecutable,
|
||||
L"-s " + QuoteArg(pythonProbePath), environment.data(), cwd, job)
|
||||
: ERROR_INVALID_HANDLE;
|
||||
DWORD curlVersionResult = jobReady && curlCopied
|
||||
? RunContained(appContainerSid, QuoteArg(curlPath) + L" --version",
|
||||
environment.data(), cwd, job)
|
||||
@@ -381,6 +543,7 @@ int wmain() {
|
||||
}
|
||||
bool stagedInputRoundTripMatches = stagedInputCopied && FilesMatch(
|
||||
L"README.md", stagedCopyPath);
|
||||
bool pythonVersionVisible = IsVersionText(pythonVersionPath);
|
||||
|
||||
if (job) CloseHandle(job);
|
||||
if (appContainerFolder) CoTaskMemFree(appContainerFolder);
|
||||
@@ -388,6 +551,8 @@ int wmain() {
|
||||
DeleteFileW(curlPath.c_str());
|
||||
DeleteFileW(stagedInputPath.c_str());
|
||||
DeleteFileW(stagedCopyPath.c_str());
|
||||
DeleteFileW(pythonProbePath.c_str());
|
||||
DeleteFileW(pythonVersionPath.c_str());
|
||||
DeleteAppContainerProfile(profileName.c_str());
|
||||
DeleteFileW(secretPath.c_str());
|
||||
DeleteFileW(writePath.c_str());
|
||||
@@ -404,12 +569,17 @@ int wmain() {
|
||||
wprintf(L"staged_copy_exit=%lu\nstaged_input_copied=%s\nstaged_roundtrip_matches=%s\n",
|
||||
stagedReadResult, stagedInputCopied ? L"true" : L"false",
|
||||
stagedInputRoundTripMatches ? L"true" : L"false");
|
||||
wprintf(L"python_runtime_copied=%s\npython_runtime_bytes=%llu\npython_runtime_files=%lu\npython_run_exit=%lu\npython_version_written=%s\n",
|
||||
pythonRuntimeCopied ? L"true" : L"false", pythonRuntimeBytes,
|
||||
pythonRuntimeFiles, pythonRunResult, pythonVersionVisible ? L"true" : L"false");
|
||||
wprintf(L"curl_version_exit=%lu\ncurl_local_health_exit=%lu\n",
|
||||
curlVersionResult, curlNetworkResult);
|
||||
if (!jobReady || shellResult != 0 || readResult == 0 || writeResult == 0 ||
|
||||
!hostSecretPreserved || !writeWasBlocked || !workspaceFolderReady ||
|
||||
allowedWriteResult != 0 || !allowedWorkspaceWriteVisible ||
|
||||
!stagedInputCopied || stagedReadResult != 0 || !stagedInputRoundTripMatches ||
|
||||
!pythonRuntimeCopied || !pythonProbeWritten || pythonRunResult != 0 ||
|
||||
!pythonVersionVisible ||
|
||||
!curlCopied || curlVersionResult != 0 || curlNetworkResult == 0) return 20;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ $outputDir = Join-Path $env:TEMP ("SovereignAI-AppContainer-" + [guid]::NewGuid(
|
||||
New-Item -ItemType Directory -Path $outputDir | Out-Null
|
||||
$exe = Join-Path $outputDir 'appcontainer_probe.exe'
|
||||
$obj = Join-Path $outputDir 'appcontainer_probe.obj'
|
||||
$previousPythonHome = $env:SOVEREIGNAI_PYTHON_HOME
|
||||
|
||||
try {
|
||||
Push-Location $repoRoot
|
||||
@@ -21,6 +22,11 @@ try {
|
||||
throw "C++ probe compilation failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$pythonHome = (& python -c 'import sys; print(sys.base_prefix)').Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $pythonHome -PathType Container)) {
|
||||
throw 'Could not locate the active Python runtime to test isolated Python execution.'
|
||||
}
|
||||
$env:SOVEREIGNAI_PYTHON_HOME = $pythonHome
|
||||
& $exe
|
||||
$probeExit = $LASTEXITCODE
|
||||
if ($probeExit -ne 0) {
|
||||
@@ -30,6 +36,11 @@ try {
|
||||
Pop-Location
|
||||
}
|
||||
} finally {
|
||||
if ($null -eq $previousPythonHome) {
|
||||
Remove-Item Env:SOVEREIGNAI_PYTHON_HOME -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:SOVEREIGNAI_PYTHON_HOME = $previousPythonHome
|
||||
}
|
||||
$resolvedOutput = (Resolve-Path -LiteralPath $outputDir -ErrorAction SilentlyContinue).Path
|
||||
$tempRoot = [IO.Path]::GetFullPath($env:TEMP).TrimEnd('\') + '\'
|
||||
if ($resolvedOutput -and $resolvedOutput.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and
|
||||
|
||||
Reference in New Issue
Block a user