Prototype AppContainer command isolation

This commit is contained in:
Hamza Ayed
2026-10-03 12:41:34 +03:00
parent 702cbdae07
commit ce70fda360
3 changed files with 404 additions and 1 deletions
+1 -1
View File
@@ -97,7 +97,7 @@
2. [x] قراءة ملف كود محدد: عند ذكر مساره النسبي مثل `app/model_provider.py` في وضع الملفات، يقرأ الوكيل مقتطفًا أكبر (حتى 12,000 حرف) ويعيد اسم الملف. (2026-10-01: تجربة API حية أعادت جوابًا عن الملف المحدد وأعادت مساره وحده كمصدر.)
3. [x] اختيار مساحة العمل/الملف من نافذة التطبيق واستعراض قائمة الملفات قبل سؤال الوكيل. تعرض الواجهة الملفات النصية المدعومة وتسمح بتحديد 3 كحد أقصى؛ يرسل التطبيق المسارات النسبية، ويتحقق الخادم منها داخل المجلد المحدد. فُحص endpoint حيًا واستُبعد `.env`؛ ولا يرسل قائمة الملفات أو محتواها للتخزين.
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 مدعوم رسميًا على Windows Pro، لكن فحص حالة الميزة يحتاج مسؤولًا؛ محاولة تشغيل استعلام DISM مرتفع الصلاحية انتهت بخطأ Windows `0xc0000142` ولم تغيّر إعدادًا. لم يُفعّل أي تنفيذ أوامر. الخطوة التالية إعداد Windows Sandbox من ميزات Windows ثم إعادة التشغيل، والتحقق من فحص smoke بلا شبكة أو clipboard وبمجلد إدخال للقراءة فقط ومجلد مخرجات مؤقت. [متطلبات Windows Sandbox](https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/) و[خيارات ملف .wsb](https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-configure-using-wsb-file).)
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 للمضيف ومن إنشاء ملف خارجه، بينما نجح في الكتابة وقراءة الناتج من مجلد العمل المعزول. اختُبر `curl.exe` داخل الحاوية (`--version` exit 0)، وفشل الوصول إلى `/health` على `127.0.0.1:8100` بمهلة curl 28 رغم نجاح endpoint من المضيف؛ هذا فحص اتصال محلي فقط، وليس اختبارًا للإنترنت العام. إعداد AppContainer بلا قدرات شبكية. الجلسة المقيدة دون elevation فشلت في إنشاء الملف الشخصي بـ`0x80070005`، لذا تشغيل الفاحص يتطلب سياق Windows صالحًا لإنشاء AppContainer. ما زال هذا 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).)
6. لا وصول عام إلى القرص، ولا أوامر مدمرة أو نشر خارجي دون موافقة صريحة. كل أداة لها مخطط مدخلات ومخرجات واختبارات وسجل تدقيق.
### كودكس للبرمجة
@@ -0,0 +1,364 @@
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <userenv.h>
#include <sddl.h>
#include <aclapi.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <algorithm>
#include <vector>
#pragma comment(lib, "userenv.lib")
#pragma comment(lib, "ole32.lib")
static bool GrantContainerFolderAccess(const std::wstring& path, PSID sid) {
PACL oldDacl = nullptr;
PSECURITY_DESCRIPTOR descriptor = nullptr;
DWORD error = GetNamedSecurityInfoW(
const_cast<LPWSTR>(path.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, &oldDacl, nullptr, &descriptor);
if (error != ERROR_SUCCESS) return false;
EXPLICIT_ACCESSW access{};
access.grfAccessPermissions = GENERIC_ALL;
access.grfAccessMode = GRANT_ACCESS;
access.grfInheritance = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE;
BuildTrusteeWithSidW(&access.Trustee, sid);
PACL updatedDacl = nullptr;
error = SetEntriesInAclW(1, &access, oldDacl, &updatedDacl);
if (error == ERROR_SUCCESS) {
error = SetNamedSecurityInfoW(
const_cast<LPWSTR>(path.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, updatedDacl, nullptr);
}
if (updatedDacl) LocalFree(updatedDacl);
if (descriptor) LocalFree(descriptor);
return error == ERROR_SUCCESS;
}
static std::wstring QuoteArg(const std::wstring& value) {
std::wstring out = L"\"";
size_t slashes = 0;
for (wchar_t ch : value) {
if (ch == L'\\') {
++slashes;
} else if (ch == L'\"') {
out.append(slashes * 2 + 1, L'\\');
out += ch;
slashes = 0;
} else {
out.append(slashes, L'\\');
slashes = 0;
out += ch;
}
}
out.append(slashes * 2, L'\\');
out += L'\"';
return out;
}
static std::vector<wchar_t> SafeEnvironmentBlock() {
static const wchar_t* const allowed[] = {
L"ALLUSERSPROFILE", L"APPDATA", L"COMSPEC", L"HOMEDRIVE", L"HOMEPATH",
L"LOCALAPPDATA", L"NUMBER_OF_PROCESSORS", L"OS", L"PATH", L"PATHEXT",
L"PROCESSOR_ARCHITECTURE", L"PROCESSOR_IDENTIFIER", L"PROGRAMDATA",
L"PROGRAMFILES", L"PROGRAMFILES(X86)", L"PUBLIC", L"SYSTEMDRIVE",
L"SYSTEMROOT", L"TEMP", L"TMP", L"USERDOMAIN", L"USERNAME", L"USERPROFILE",
L"WINDIR"
};
std::vector<std::wstring> entries;
LPWCH inherited = GetEnvironmentStringsW();
if (!inherited) return {};
for (const wchar_t* item = inherited; *item;) {
std::wstring entry(item);
item += entry.size() + 1;
size_t separator = entry.find(L'=');
if (separator == std::wstring::npos || separator == 0) continue;
std::wstring key = entry.substr(0, separator);
bool keep = false;
for (const wchar_t* candidate : allowed) {
if (_wcsicmp(key.c_str(), candidate) == 0) {
keep = true;
break;
}
}
if (keep) entries.push_back(std::move(entry));
}
FreeEnvironmentStringsW(inherited);
std::sort(entries.begin(), entries.end(), [](const std::wstring& left, const std::wstring& right) {
return _wcsicmp(left.c_str(), right.c_str()) < 0;
});
std::vector<wchar_t> block;
for (const auto& entry : entries) {
block.insert(block.end(), entry.begin(), entry.end());
block.push_back(L'\0');
}
block.push_back(L'\0');
if (entries.empty()) block.push_back(L'\0');
return block;
}
static void SetEnvironmentValue(
std::vector<wchar_t>& block, const std::wstring& key, const std::wstring& value) {
std::vector<std::wstring> entries;
for (const wchar_t* item = block.data(); item && *item;) {
std::wstring entry(item);
item += entry.size() + 1;
size_t separator = entry.find(L'=');
if (separator == std::wstring::npos || _wcsicmp(entry.substr(0, separator).c_str(), key.c_str()) != 0) {
entries.push_back(std::move(entry));
}
}
entries.push_back(key + L"=" + value);
std::sort(entries.begin(), entries.end(), [](const std::wstring& left, const std::wstring& right) {
return _wcsicmp(left.c_str(), right.c_str()) < 0;
});
block.clear();
for (const auto& entry : entries) {
block.insert(block.end(), entry.begin(), entry.end());
block.push_back(L'\0');
}
block.push_back(L'\0');
}
static DWORD RunContained(
PSID appContainerSid,
const std::wstring& command,
const wchar_t* environment,
const std::wstring& cwd,
HANDLE job
) {
SIZE_T attributeBytes = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeBytes);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return GetLastError();
auto* attributes = static_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(
HeapAlloc(GetProcessHeap(), 0, attributeBytes)
);
if (!attributes) return ERROR_OUTOFMEMORY;
if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attributeBytes)) {
DWORD error = GetLastError();
HeapFree(GetProcessHeap(), 0, attributes);
return error;
}
SECURITY_CAPABILITIES security{};
security.AppContainerSid = appContainerSid;
security.Capabilities = nullptr;
security.CapabilityCount = 0;
if (!UpdateProcThreadAttribute(
attributes,
0,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
&security,
sizeof(security),
nullptr,
nullptr)) {
DWORD error = GetLastError();
DeleteProcThreadAttributeList(attributes);
HeapFree(GetProcessHeap(), 0, attributes);
return error;
}
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
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 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",
mutableLine.empty() ? nullptr : &mutableLine[0],
nullptr,
nullptr,
FALSE,
flags,
const_cast<wchar_t*>(environment),
cwd.c_str(),
&startup.StartupInfo,
&process
);
DWORD error = created ? ERROR_SUCCESS : GetLastError();
DeleteProcThreadAttributeList(attributes);
HeapFree(GetProcessHeap(), 0, attributes);
if (!created) return error;
if (!AssignProcessToJobObject(job, process.hProcess)) {
error = GetLastError();
TerminateProcess(process.hProcess, error);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return error;
}
ResumeThread(process.hThread);
DWORD waitResult = WaitForSingleObject(process.hProcess, 30000);
DWORD exitCode = ERROR_TIMEOUT;
if (waitResult == WAIT_OBJECT_0) {
GetExitCodeProcess(process.hProcess, &exitCode);
} else {
TerminateJobObject(job, ERROR_TIMEOUT);
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return exitCode;
}
int wmain() {
wchar_t tempPath[MAX_PATH]{};
if (!GetTempPathW(MAX_PATH, tempPath)) return 10;
GUID id{};
if (FAILED(CoCreateGuid(&id))) return 11;
wchar_t idText[40]{};
if (StringFromGUID2(id, idText, 40) == 0) return 12;
std::wstring nonce = idText;
if (!nonce.empty() && nonce.front() == L'{') nonce.erase(nonce.begin());
if (!nonce.empty() && nonce.back() == L'}') nonce.pop_back();
std::wstring profileName = L"SovereignAIProbe" + nonce;
std::wstring secretPath = std::wstring(tempPath) + L"SovereignAIProbe" + nonce + L".txt";
std::wstring writePath = std::wstring(tempPath) + L"SovereignAIProbe" + nonce + L"-write.txt";
const char marker[] = "host-secret-must-stay-private";
HANDLE secret = CreateFileW(secretPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
FILE_ATTRIBUTE_TEMPORARY, nullptr);
if (secret == INVALID_HANDLE_VALUE) return 13;
DWORD bytesWritten = 0;
if (!WriteFile(secret, marker, sizeof(marker) - 1, &bytesWritten, nullptr) ||
bytesWritten != sizeof(marker) - 1) {
CloseHandle(secret);
return 14;
}
CloseHandle(secret);
PSID appContainerSid = nullptr;
HRESULT profileResult = CreateAppContainerProfile(
profileName.c_str(), L"SovereignAI isolated command probe",
L"Temporary test profile for command isolation", nullptr, 0, &appContainerSid);
if (FAILED(profileResult)) {
fwprintf(stderr, L"CreateAppContainerProfile failed: 0x%08lx (Win32 %lu)\n",
static_cast<unsigned long>(profileResult),
static_cast<unsigned long>(HRESULT_CODE(profileResult)));
DeleteFileW(secretPath.c_str());
return static_cast<int>(HRESULT_CODE(profileResult));
}
LPWSTR sidText = nullptr;
PWSTR appContainerFolder = nullptr;
bool appContainerPathReady = ConvertSidToStringSidW(appContainerSid, &sidText) &&
SUCCEEDED(GetAppContainerFolderPath(sidText, &appContainerFolder));
if (!appContainerPathReady) {
FreeSid(appContainerSid);
DeleteAppContainerProfile(profileName.c_str());
DeleteFileW(secretPath.c_str());
return 16;
}
if (sidText) LocalFree(sidText);
HANDLE job = CreateJobObjectW(nullptr, nullptr);
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_JOB_MEMORY;
limits.BasicLimitInformation.ActiveProcessLimit = 8;
limits.JobMemoryLimit = 512ull * 1024 * 1024;
bool jobReady = job && SetInformationJobObject(
job, JobObjectExtendedLimitInformation, &limits, sizeof(limits));
auto environment = SafeEnvironmentBlock();
if (environment.empty()) return 15;
std::wstring appDataPath(appContainerFolder);
std::wstring appTempPath = appDataPath + L"\\Temp";
std::wstring sandboxWorkspacePath = appDataPath + L"\\agent-workspace";
CreateDirectoryW(appTempPath.c_str(), nullptr);
bool workspaceFolderReady = CreateDirectoryW(sandboxWorkspacePath.c_str(), nullptr) != FALSE ||
GetLastError() == ERROR_ALREADY_EXISTS;
workspaceFolderReady = workspaceFolderReady &&
GrantContainerFolderAccess(sandboxWorkspacePath, appContainerSid);
std::wstring curlPath = sandboxWorkspacePath + L"\\curl.exe";
bool curlCopied = workspaceFolderReady &&
CopyFileW(L"C:\\Windows\\System32\\curl.exe", curlPath.c_str(), FALSE);
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);
std::wstring cwd = L"C:\\Windows\\System32";
DWORD shellResult = jobReady
? RunContained(appContainerSid, L"exit 0", environment.data(), cwd, job)
: GetLastError();
std::wstring readAttempt = L"type " + secretPath + L" >nul 2>nul";
DWORD readResult = jobReady
? RunContained(appContainerSid, readAttempt, environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
std::wstring writeAttempt = L"echo modified>" + writePath + L" 2>nul";
DWORD writeResult = jobReady
? RunContained(appContainerSid, writeAttempt, environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
std::wstring allowedWritePath = sandboxWorkspacePath + L"\\probe-output.txt";
DWORD allowedWriteResult = jobReady && workspaceFolderReady
? RunContained(appContainerSid, L"echo contained>" + allowedWritePath,
environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
DWORD curlVersionResult = jobReady && curlCopied
? RunContained(appContainerSid, QuoteArg(curlPath) + L" --version",
environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
DWORD curlNetworkResult = jobReady && curlCopied
? RunContained(appContainerSid, QuoteArg(curlPath) +
L" --noproxy \"*\" --max-time 4 http://127.0.0.1:8100/health -o NUL",
environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
DWORD attrs = GetFileAttributesW(writePath.c_str());
DWORD attrsError = attrs == INVALID_FILE_ATTRIBUTES ? GetLastError() : ERROR_SUCCESS;
bool hostSecretPreserved = false;
secret = CreateFileW(secretPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (secret != INVALID_HANDLE_VALUE) {
char check[sizeof(marker)]{};
DWORD bytesRead = 0;
hostSecretPreserved = ReadFile(secret, check, sizeof(marker) - 1, &bytesRead, nullptr) &&
bytesRead == sizeof(marker) - 1 && memcmp(check, marker, sizeof(marker) - 1) == 0;
CloseHandle(secret);
}
bool allowedWorkspaceWriteVisible = false;
HANDLE allowedOutput = CreateFileW(allowedWritePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (allowedOutput != INVALID_HANDLE_VALUE) {
char contents[32]{};
DWORD count = 0;
allowedWorkspaceWriteVisible = ReadFile(allowedOutput, contents, sizeof(contents) - 1,
&count, nullptr) && count >= 9 && memcmp(contents, "contained", 9) == 0;
CloseHandle(allowedOutput);
}
if (job) CloseHandle(job);
if (appContainerFolder) CoTaskMemFree(appContainerFolder);
FreeSid(appContainerSid);
DeleteFileW(curlPath.c_str());
DeleteAppContainerProfile(profileName.c_str());
DeleteFileW(secretPath.c_str());
DeleteFileW(writePath.c_str());
DeleteFileW(allowedWritePath.c_str());
bool writeWasBlocked = attrs == INVALID_FILE_ATTRIBUTES &&
(attrsError == ERROR_FILE_NOT_FOUND || attrsError == ERROR_ACCESS_DENIED);
wprintf(L"shell_exit=%lu\nread_host_file_exit=%lu\nwrite_host_file_exit=%lu\nhost_secret_preserved=%s\nwrite_artifact_absent=%s\n",
shellResult, readResult, writeResult,
hostSecretPreserved ? L"true" : L"false",
writeWasBlocked ? L"true" : L"false");
wprintf(L"profile_write_exit=%lu\nprofile_write_visible=%s\n",
allowedWriteResult, allowedWorkspaceWriteVisible ? 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 ||
!curlCopied || curlVersionResult != 0 || curlNetworkResult == 0) return 20;
return 0;
}
@@ -0,0 +1,39 @@
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$source = Join-Path $PSScriptRoot 'appcontainer_probe.cpp'
$vcvars = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat'
if (-not (Test-Path -LiteralPath $vcvars)) {
throw "Visual Studio C++ Build Tools not found: $vcvars"
}
$outputDir = Join-Path $env:TEMP ("SovereignAI-AppContainer-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $outputDir | Out-Null
$exe = Join-Path $outputDir 'appcontainer_probe.exe'
$obj = Join-Path $outputDir 'appcontainer_probe.obj'
try {
Push-Location $repoRoot
try {
$compile = 'call "{0}" && cl.exe /nologo /EHsc /W4 /Fo:"{1}" "{2}" /Fe:"{3}" /link Userenv.lib Ole32.lib Advapi32.lib' -f $vcvars, $obj, $source, $exe
& cmd.exe /d /s /c $compile
if ($LASTEXITCODE -ne 0) {
throw "C++ probe compilation failed with exit code $LASTEXITCODE"
}
& $exe
$probeExit = $LASTEXITCODE
if ($probeExit -ne 0) {
throw "AppContainer probe failed with exit code $probeExit"
}
} finally {
Pop-Location
}
} finally {
$resolvedOutput = (Resolve-Path -LiteralPath $outputDir -ErrorAction SilentlyContinue).Path
$tempRoot = [IO.Path]::GetFullPath($env:TEMP).TrimEnd('\') + '\'
if ($resolvedOutput -and $resolvedOutput.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -and
(Split-Path -Leaf $resolvedOutput).StartsWith('SovereignAI-AppContainer-', [StringComparison]::Ordinal)) {
Remove-Item -LiteralPath $resolvedOutput -Recurse -Force
}
}