From 5652db5dbc0bf5a3d27ed8de65c21e26c2ce8278 Mon Sep 17 00:00:00 2001 From: Hamza Ayed Date: Sat, 3 Oct 2026 13:17:47 +0300 Subject: [PATCH] Run staged Python file in AppContainer probe --- SovereignAI-Starter/ROADMAP.md | 6 +- .../scripts/appcontainer_probe.cpp | 344 +++++++++++++++--- .../scripts/appcontainer_user_code_smoke.py | 11 + .../scripts/run_appcontainer_probe.ps1 | 3 +- .../scripts/run_appcontainer_probe.py | 38 ++ 5 files changed, 354 insertions(+), 48 deletions(-) create mode 100644 SovereignAI-Starter/scripts/appcontainer_user_code_smoke.py create mode 100644 SovereignAI-Starter/scripts/run_appcontainer_probe.py diff --git a/SovereignAI-Starter/ROADMAP.md b/SovereignAI-Starter/ROADMAP.md index 606317a..32526d9 100644 --- a/SovereignAI-Starter/ROADMAP.md +++ b/SovereignAI-Starter/ROADMAP.md @@ -98,9 +98,9 @@ 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 يحتاج مسؤولًا؛ محاولة 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 والواجهة. + - [x] تجهيز Snapshot محدود لملفات يختارها المستخدم (`app/execution_snapshot.py`): يفرض جذر workspace المعتمد للحساب، حتى 50 ملفًا، 512KB لكل ملف و10MB إجماليًا، ويقبل الامتدادات المدعومة فقط. يرفض المسارات المخفية/المستثناة/الخارجة، والروابط الرمزية، وأسماء أجهزة Windows المحجوزة، وأسماء الملفات/المحتوى التي تكشف مفاتيح معروفة أو قيم اعتماد مباشرة؛ وينسخ إلى مجلد مؤقت مع SHA-256 لكل ملف. 7 اختبارات snapshot و7 اختبارات workspace ناجحة (2026-10-03). فحص الأسرار محافظ وليس ماسحًا شاملًا ولا يغني عن المراجعة. مرّ الـSnapshot عبر driver تطوير تجريبي إلى broker، لكنه غير مربوط بعد بطلب API أو منتقي ملفات المستخدم. + - [x] تشغيل Python من ملفات Snapshot في AppContainer: نُسخت ملفات التشغيل القياسية وDLLs من Python 3.14 (33,627,970 بايت/631 ملفًا، دون `site-packages`)، وشغّل broker ملف `.py` الموجود داخل Snapshot وأعاد ملف نتيجة؛ تحقق الخروج `0`. يلتقط stdout/stderr عبر pipe ويخزن أول 64KB فقط: اختبار خرج 70KB أعاد 65,536 بايت وعلامة `truncated=true`. لم نختبر بعد مخرجات stderr غير UTF-8 أو الكتابة الكبيرة على القرص، والملف المشغل الحالي fixture تجريبي من المشروع. + - [ ] إكمال الربط الإنتاجي: endpoint مصادق عليه لخطة أمر allowlist وموافقة مرة واحدة مرتبطة بالمستخدم والمساحة والبصمات والمهلة؛ نسخ الملفات المحددة من API إلى broker، حد حجم القرص لمخرجات التنفيذ، capture موحد وآمن للنتيجة، عرض الموافقة والحالة والمخرجات في Flutter، ومعاينة diff قبل أي تطبيق. prototype لم يُدمج في API أو الواجهة ولا يسمح حاليًا بتنفيذ أوامر المستخدم. 6. لا وصول عام إلى القرص، ولا أوامر مدمرة أو نشر خارجي دون موافقة صريحة. كل أداة لها مخطط مدخلات ومخرجات واختبارات وسجل تدقيق. ### كودكس للبرمجة diff --git a/SovereignAI-Starter/scripts/appcontainer_probe.cpp b/SovereignAI-Starter/scripts/appcontainer_probe.cpp index 17ee4fb..d09fd3a 100644 --- a/SovereignAI-Starter/scripts/appcontainer_probe.cpp +++ b/SovereignAI-Starter/scripts/appcontainer_probe.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -214,6 +215,120 @@ static bool CopyRuntimeTree( return success && finalError == ERROR_NO_MORE_FILES; } +static constexpr ULONGLONG kMaxSnapshotBytes = 10ull * 1024 * 1024; +static constexpr DWORD kMaxSnapshotFiles = 50; + +static bool SnapshotExtensionAllowed(const std::wstring& name) { + const size_t dot = name.find_last_of(L'.'); + if (dot == std::wstring::npos) return false; + std::wstring extension = name.substr(dot); + for (wchar_t& ch : extension) ch = static_cast(towlower(ch)); + static const wchar_t* const allowed[] = { + L".py", L".dart", L".md", L".txt", L".json", L".yaml", L".yml", + L".toml", L".html", L".css", L".js", L".ts", L".tsx", L".jsx", + L".sh", L".ps1" + }; + for (const wchar_t* candidate : allowed) { + if (extension == candidate) return true; + } + return false; +} + +static bool CopySnapshotTree( + const std::wstring& source, const std::wstring& destination, + ULONGLONG& copiedBytes, DWORD& copiedFiles, unsigned depth = 0 +) { + if (depth > 24 || copiedFiles > kMaxSnapshotFiles) return false; + const DWORD sourceAttributes = GetFileAttributesW(source.c_str()); + if (sourceAttributes == INVALID_FILE_ATTRIBUTES || + !(sourceAttributes & FILE_ATTRIBUTE_DIRECTORY) || + (sourceAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) 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) || entry.cFileName[0] == L'.') { + success = false; + break; + } + const std::wstring sourcePath = source + L"\\" + entry.cFileName; + const std::wstring destinationPath = destination + L"\\" + entry.cFileName; + if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + success = CopySnapshotTree(sourcePath, destinationPath, + copiedBytes, copiedFiles, depth + 1); + } else { + if (!SnapshotExtensionAllowed(entry.cFileName)) { + success = false; + break; + } + WIN32_FILE_ATTRIBUTE_DATA attributes{}; + if (!GetFileAttributesExW(sourcePath.c_str(), GetFileExInfoStandard, &attributes) || + (attributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) { + success = false; + break; + } + const ULONGLONG size = (static_cast(attributes.nFileSizeHigh) << 32) | + attributes.nFileSizeLow; + if (copiedFiles >= kMaxSnapshotFiles || size > kMaxSnapshotBytes - copiedBytes || + !CopyFileW(sourcePath.c_str(), destinationPath.c_str(), FALSE)) { + success = false; + break; + } + copiedBytes += size; + ++copiedFiles; + } + } while (FindNextFileW(search, &entry)); + const DWORD finalError = GetLastError(); + FindClose(search); + return success && finalError == ERROR_NO_MORE_FILES; +} + +static std::wstring ReadEnvironmentString(const wchar_t* name) { + std::vector buffer(32768); + DWORD length = GetEnvironmentVariableW(name, buffer.data(), + static_cast(buffer.size())); + if (length == 0 || length >= buffer.size()) return {}; + return std::wstring(buffer.data(), length); +} + +static bool ResolvePythonEntry( + const std::wstring& projectRoot, std::wstring relative, std::wstring& resolved +) { + if (relative.empty() || relative.size() > 240 || relative.front() == L'/' || + relative.front() == L'\\' || relative.find(L':') != std::wstring::npos) return false; + for (wchar_t& ch : relative) if (ch == L'\\') ch = L'/'; + std::wstring current = projectRoot; + size_t start = 0; + bool finalPart = false; + while (!finalPart) { + size_t separator = relative.find(L'/', start); + finalPart = separator == std::wstring::npos; + const std::wstring part = relative.substr(start, + finalPart ? std::wstring::npos : separator - start); + if (part.empty() || part == L"." || part == L".." || part.front() == L'.' || + part.find_first_of(L"<>|?*\"") != std::wstring::npos) return false; + current += L"\\" + part; + const DWORD attributes = GetFileAttributesW(current.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || + (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) return false; + if (!finalPart && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) return false; + if (finalPart && (attributes & FILE_ATTRIBUTE_DIRECTORY)) return false; + start = separator + 1; + } + const size_t dot = relative.find_last_of(L'.'); + if (dot == std::wstring::npos) return false; + std::wstring extension = relative.substr(dot); + for (wchar_t& ch : extension) ch = static_cast(towlower(ch)); + if (extension != L".py") return false; + resolved = std::move(current); + return true; +} + static bool CopyPythonRuntime( const std::wstring& sourceRoot, const std::wstring& destinationRoot, ULONGLONG& copiedBytes, DWORD& copiedFiles @@ -243,27 +358,20 @@ static bool CopyPythonRuntime( 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) { +static bool IsSmokeResult(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); + count >= 20 && count < sizeof(contents); CloseHandle(file); + const char prefix[] = "sandbox-python:"; + valid = valid && count > sizeof(prefix) - 1 && + memcmp(contents, prefix, sizeof(prefix) - 1) == 0; bool sawDot = false; - for (DWORD index = 0; valid && index < count; ++index) { + for (DWORD index = sizeof(prefix) - 1; valid && index < count; ++index) { if (contents[index] == '.') sawDot = true; else if (contents[index] < '0' || contents[index] > '9') valid = false; } @@ -276,19 +384,63 @@ static DWORD RunContainedExe( const std::wstring& arguments, const wchar_t* environment, const std::wstring& cwd, - HANDLE job + HANDLE job, + std::string* capturedOutput = nullptr, + bool* outputTruncated = nullptr ) { + constexpr size_t kMaxCapturedOutput = 64 * 1024; + const bool capture = capturedOutput != nullptr; + if (capturedOutput) capturedOutput->clear(); + if (outputTruncated) *outputTruncated = false; + + HANDLE pipeRead = nullptr; + HANDLE pipeWrite = nullptr; + HANDLE nullInput = nullptr; + if (capture) { + SECURITY_ATTRIBUTES pipeSecurity{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + if (!CreatePipe(&pipeRead, &pipeWrite, &pipeSecurity, 0) || + !SetHandleInformation(pipeRead, HANDLE_FLAG_INHERIT, 0)) { + DWORD error = GetLastError(); + if (pipeRead) CloseHandle(pipeRead); + if (pipeWrite) CloseHandle(pipeWrite); + return error; + } + nullInput = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + &pipeSecurity, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (nullInput == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + CloseHandle(pipeRead); + CloseHandle(pipeWrite); + return error; + } + } + + const DWORD attributeCount = capture ? 2 : 1; SIZE_T attributeBytes = 0; - InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeBytes); - if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return GetLastError(); + InitializeProcThreadAttributeList(nullptr, attributeCount, 0, &attributeBytes); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + DWORD error = GetLastError(); + if (pipeRead) CloseHandle(pipeRead); + if (pipeWrite) CloseHandle(pipeWrite); + if (nullInput) CloseHandle(nullInput); + return error; + } auto* attributes = static_cast( HeapAlloc(GetProcessHeap(), 0, attributeBytes) ); - if (!attributes) return ERROR_OUTOFMEMORY; - if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attributeBytes)) { + if (!attributes) { + if (pipeRead) CloseHandle(pipeRead); + if (pipeWrite) CloseHandle(pipeWrite); + if (nullInput) CloseHandle(nullInput); + return ERROR_OUTOFMEMORY; + } + if (!InitializeProcThreadAttributeList(attributes, attributeCount, 0, &attributeBytes)) { DWORD error = GetLastError(); HeapFree(GetProcessHeap(), 0, attributes); + if (pipeRead) CloseHandle(pipeRead); + if (pipeWrite) CloseHandle(pipeWrite); + if (nullInput) CloseHandle(nullInput); return error; } @@ -308,13 +460,37 @@ static DWORD RunContainedExe( DWORD error = GetLastError(); DeleteProcThreadAttributeList(attributes); HeapFree(GetProcessHeap(), 0, attributes); + if (pipeRead) CloseHandle(pipeRead); + if (pipeWrite) CloseHandle(pipeWrite); + if (nullInput) CloseHandle(nullInput); return error; } + if (capture) { + HANDLE inheritedHandles[] = {nullInput, pipeWrite}; + if (!UpdateProcThreadAttribute( + attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inheritedHandles, sizeof(inheritedHandles), nullptr, nullptr)) { + DWORD error = GetLastError(); + DeleteProcThreadAttributeList(attributes); + HeapFree(GetProcessHeap(), 0, attributes); + CloseHandle(pipeRead); + CloseHandle(pipeWrite); + CloseHandle(nullInput); + return error; + } + } + STARTUPINFOEXW startup{}; startup.StartupInfo.cb = sizeof(startup); startup.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; startup.StartupInfo.wShowWindow = SW_HIDE; + if (capture) { + startup.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = nullInput; + startup.StartupInfo.hStdOutput = pipeWrite; + startup.StartupInfo.hStdError = pipeWrite; + } startup.lpAttributeList = attributes; PROCESS_INFORMATION process{}; std::wstring commandLine = QuoteArg(application); @@ -328,7 +504,7 @@ static DWORD RunContainedExe( mutableLine.empty() ? nullptr : &mutableLine[0], nullptr, nullptr, - FALSE, + capture ? TRUE : FALSE, flags, const_cast(environment), cwd.c_str(), @@ -338,25 +514,74 @@ static DWORD RunContainedExe( DWORD error = created ? ERROR_SUCCESS : GetLastError(); DeleteProcThreadAttributeList(attributes); HeapFree(GetProcessHeap(), 0, attributes); - if (!created) return error; + if (pipeWrite) CloseHandle(pipeWrite); + if (nullInput) CloseHandle(nullInput); + if (!created) { + if (pipeRead) CloseHandle(pipeRead); + return error; + } if (!AssignProcessToJobObject(job, process.hProcess)) { error = GetLastError(); TerminateProcess(process.hProcess, error); CloseHandle(process.hThread); CloseHandle(process.hProcess); + if (pipeRead) CloseHandle(pipeRead); return error; } ResumeThread(process.hThread); - DWORD waitResult = WaitForSingleObject(process.hProcess, 30000); DWORD exitCode = ERROR_TIMEOUT; - if (waitResult == WAIT_OBJECT_0) { - GetExitCodeProcess(process.hProcess, &exitCode); + if (capture) { + const ULONGLONG deadline = GetTickCount64() + 30000; + bool processFinished = false; + bool pipeFinished = false; + while (!pipeFinished || !processFinished) { + DWORD available = 0; + if (!pipeFinished && PeekNamedPipe(pipeRead, nullptr, 0, nullptr, &available, nullptr)) { + while (available > 0) { + char buffer[4096]; + DWORD bytesRead = 0; + const DWORD requested = (std::min)(available, static_cast(sizeof(buffer))); + if (!ReadFile(pipeRead, buffer, requested, &bytesRead, nullptr) || bytesRead == 0) { + pipeFinished = true; + break; + } + const size_t remaining = capturedOutput->size() < kMaxCapturedOutput + ? kMaxCapturedOutput - capturedOutput->size() : 0; + const size_t retained = (std::min)(remaining, static_cast(bytesRead)); + capturedOutput->append(buffer, retained); + if (retained < bytesRead && outputTruncated) *outputTruncated = true; + available -= bytesRead; + } + } else if (GetLastError() == ERROR_BROKEN_PIPE) { + pipeFinished = true; + } + + if (!processFinished && WaitForSingleObject(process.hProcess, 0) == WAIT_OBJECT_0) { + GetExitCodeProcess(process.hProcess, &exitCode); + processFinished = true; + TerminateJobObject(job, ERROR_SUCCESS); + } + if (!processFinished && GetTickCount64() >= deadline) { + TerminateJobObject(job, ERROR_TIMEOUT); + exitCode = ERROR_TIMEOUT; + processFinished = WaitForSingleObject(process.hProcess, 5000) == WAIT_OBJECT_0; + pipeFinished = false; + } + if (!pipeFinished || !processFinished) Sleep(20); + if (processFinished && pipeFinished) break; + } } else { - TerminateJobObject(job, ERROR_TIMEOUT); + DWORD waitResult = WaitForSingleObject(process.hProcess, 30000); + if (waitResult == WAIT_OBJECT_0) { + GetExitCodeProcess(process.hProcess, &exitCode); + } else { + TerminateJobObject(job, ERROR_TIMEOUT); + } } CloseHandle(process.hThread); CloseHandle(process.hProcess); + if (pipeRead) CloseHandle(pipeRead); return exitCode; } @@ -434,18 +659,25 @@ int wmain() { std::wstring appDataPath(appContainerFolder); std::wstring appTempPath = appDataPath + L"\\Temp"; std::wstring sandboxWorkspacePath = appDataPath + L"\\agent-workspace"; - std::wstring stagedInputPath = sandboxWorkspacePath + L"\\README.md"; - std::wstring stagedCopyPath = sandboxWorkspacePath + L"\\staged-copy.md"; + std::wstring projectSnapshotPath = sandboxWorkspacePath + L"\\project"; + std::wstring stagedInputPath = projectSnapshotPath + L"\\README.md"; + std::wstring stagedCopyPath = projectSnapshotPath + 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"; + std::wstring pythonVersionPath = projectSnapshotPath + L"\\scripts\\execution-result.txt"; CreateDirectoryW(appTempPath.c_str(), nullptr); bool workspaceFolderReady = CreateDirectoryW(sandboxWorkspacePath.c_str(), nullptr) != FALSE || GetLastError() == ERROR_ALREADY_EXISTS; workspaceFolderReady = workspaceFolderReady && GrantContainerFolderAccess(sandboxWorkspacePath, appContainerSid); - bool stagedInputCopied = workspaceFolderReady && - CopyFileW(L"README.md", stagedInputPath.c_str(), FALSE); + const std::wstring hostSnapshotPath = ReadEnvironmentString(L"SOVEREIGNAI_STAGING_ROOT"); + const std::wstring requestedEntry = ReadEnvironmentString(L"SOVEREIGNAI_ENTRY_SCRIPT"); + ULONGLONG stagedBytes = 0; + DWORD stagedFiles = 0; + bool stagedInputCopied = workspaceFolderReady && !hostSnapshotPath.empty() && + CopySnapshotTree(hostSnapshotPath, projectSnapshotPath, stagedBytes, stagedFiles); + std::wstring pythonEntryPath; + bool entryScriptReady = stagedInputCopied && + ResolvePythonEntry(projectSnapshotPath, requestedEntry, pythonEntryPath); std::wstring curlPath = sandboxWorkspacePath + L"\\curl.exe"; bool curlCopied = workspaceFolderReady && CopyFileW(L"C:\\Windows\\System32\\curl.exe", curlPath.c_str(), FALSE); @@ -459,12 +691,7 @@ int wmain() { 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); + bool pythonProbeReady = pythonRuntimeCopied && entryScriptReady; SetEnvironmentValue(environment, L"PATH", L"C:\\Windows\\System32"); SetEnvironmentValue(environment, L"APPDATA", appDataPath); SetEnvironmentValue(environment, L"LOCALAPPDATA", appDataPath); @@ -479,6 +706,7 @@ int wmain() { SetEnvironmentValue(environment, L"PYTHONHOME", pythonSandboxPath); SetEnvironmentValue(environment, L"PYTHONNOUSERSITE", L"1"); SetEnvironmentValue(environment, L"PYTHONDONTWRITEBYTECODE", L"1"); + SetEnvironmentValue(environment, L"PYTHONUTF8", L"1"); std::wstring cwd = L"C:\\Windows\\System32"; DWORD shellResult = jobReady ? RunContained(appContainerSid, L"exit 0", environment.data(), cwd, job) @@ -505,9 +733,19 @@ int wmain() { stagedFileUrl + L" -o " + stagedCopyPath, environment.data(), cwd, job) : ERROR_INVALID_HANDLE; std::wstring pythonExecutable = pythonSandboxPath + L"\\python.exe"; - DWORD pythonRunResult = jobReady && pythonProbeWritten + std::string pythonOutput; + bool pythonOutputTruncated = false; + DWORD pythonRunResult = jobReady && pythonProbeReady ? RunContainedExe(appContainerSid, pythonExecutable, - L"-s " + QuoteArg(pythonProbePath), environment.data(), cwd, job) + L"-s " + QuoteArg(pythonEntryPath), environment.data(), cwd, job, + &pythonOutput, &pythonOutputTruncated) + : ERROR_INVALID_HANDLE; + std::string overflowOutput; + bool overflowTruncated = false; + DWORD overflowRunResult = jobReady && pythonRuntimeCopied + ? RunContainedExe(appContainerSid, pythonExecutable, + L"-s -c " + QuoteArg(L"print('x' * 70000)"), environment.data(), cwd, + job, &overflowOutput, &overflowTruncated) : ERROR_INVALID_HANDLE; DWORD curlVersionResult = jobReady && curlCopied ? RunContained(appContainerSid, QuoteArg(curlPath) + L" --version", @@ -543,7 +781,7 @@ int wmain() { } bool stagedInputRoundTripMatches = stagedInputCopied && FilesMatch( L"README.md", stagedCopyPath); - bool pythonVersionVisible = IsVersionText(pythonVersionPath); + bool pythonVersionVisible = IsSmokeResult(pythonVersionPath); if (job) CloseHandle(job); if (appContainerFolder) CoTaskMemFree(appContainerFolder); @@ -551,7 +789,6 @@ 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()); @@ -566,20 +803,39 @@ int wmain() { writeWasBlocked ? L"true" : L"false"); wprintf(L"profile_write_exit=%lu\nprofile_write_visible=%s\n", allowedWriteResult, allowedWorkspaceWriteVisible ? L"true" : L"false"); - wprintf(L"staged_copy_exit=%lu\nstaged_input_copied=%s\nstaged_roundtrip_matches=%s\n", + wprintf(L"staged_copy_exit=%lu\nstaged_input_copied=%s\nstaged_files=%lu\nstaged_bytes=%llu\nstaged_roundtrip_matches=%s\n", stagedReadResult, stagedInputCopied ? L"true" : L"false", + stagedFiles, stagedBytes, 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", + wprintf(L"python_runtime_copied=%s\npython_runtime_bytes=%llu\npython_runtime_files=%lu\nentry_script_valid=%s\npython_run_exit=%lu\npython_result_written=%s\n", pythonRuntimeCopied ? L"true" : L"false", pythonRuntimeBytes, - pythonRuntimeFiles, pythonRunResult, pythonVersionVisible ? L"true" : L"false"); + pythonRuntimeFiles, entryScriptReady ? L"true" : L"false", + pythonRunResult, pythonVersionVisible ? L"true" : L"false"); + std::vector pythonOutputWide(pythonOutput.size() + 1); + if (!pythonOutput.empty()) { + int wideCount = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + pythonOutput.data(), static_cast(pythonOutput.size()), + pythonOutputWide.data(), static_cast(pythonOutputWide.size())); + if (wideCount > 0) pythonOutputWide[wideCount] = L'\0'; + else pythonOutputWide[0] = L'\0'; + } + wprintf(L"python_output_bytes=%llu\npython_output_truncated=%s\npython_output=%ls\n", + static_cast(pythonOutput.size()), + pythonOutputTruncated ? L"true" : L"false", + pythonOutputWide.data()); + wprintf(L"overflow_run_exit=%lu\noverflow_output_bytes=%llu\noverflow_truncated=%s\n", + overflowRunResult, static_cast(overflowOutput.size()), + overflowTruncated ? 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 || + !pythonRuntimeCopied || !pythonProbeReady || pythonRunResult != 0 || !pythonVersionVisible || + overflowRunResult != 0 || overflowOutput.size() != 64 * 1024 || + !overflowTruncated || !curlCopied || curlVersionResult != 0 || curlNetworkResult == 0) return 20; return 0; } diff --git a/SovereignAI-Starter/scripts/appcontainer_user_code_smoke.py b/SovereignAI-Starter/scripts/appcontainer_user_code_smoke.py new file mode 100644 index 0000000..47203e9 --- /dev/null +++ b/SovereignAI-Starter/scripts/appcontainer_user_code_smoke.py @@ -0,0 +1,11 @@ +"""Harmless smoke payload used only to verify staged Python execution.""" + +from pathlib import Path +import sys + +result = Path(__file__).with_name("execution-result.txt") +result.write_text( + "sandbox-python:" + ".".join(map(str, sys.version_info[:3])), + encoding="ascii", +) +print("sandbox-python-smoke-ok") diff --git a/SovereignAI-Starter/scripts/run_appcontainer_probe.ps1 b/SovereignAI-Starter/scripts/run_appcontainer_probe.ps1 index 8046417..19f4868 100644 --- a/SovereignAI-Starter/scripts/run_appcontainer_probe.ps1 +++ b/SovereignAI-Starter/scripts/run_appcontainer_probe.ps1 @@ -27,7 +27,8 @@ try { throw 'Could not locate the active Python runtime to test isolated Python execution.' } $env:SOVEREIGNAI_PYTHON_HOME = $pythonHome - & $exe + $driver = Join-Path $PSScriptRoot 'run_appcontainer_probe.py' + & python $driver $exe $probeExit = $LASTEXITCODE if ($probeExit -ne 0) { throw "AppContainer probe failed with exit code $probeExit" diff --git a/SovereignAI-Starter/scripts/run_appcontainer_probe.py b/SovereignAI-Starter/scripts/run_appcontainer_probe.py new file mode 100644 index 0000000..2ebe036 --- /dev/null +++ b/SovereignAI-Starter/scripts/run_appcontainer_probe.py @@ -0,0 +1,38 @@ +"""Stage a tiny workspace and pass it to the Windows AppContainer probe.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +REPOSITORY = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPOSITORY)) + +from app.execution_snapshot import stage_selected_files + + +def main() -> int: + if len(sys.argv) != 2: + print("usage: run_appcontainer_probe.py ", file=sys.stderr) + return 2 + repository = REPOSITORY + os.environ["SOVEREIGNAI_ALLOWED_WORKSPACES"] = str(repository) + os.environ["SOVEREIGNAI_WORKSPACE"] = str(repository) + files = ["README.md", "scripts/appcontainer_user_code_smoke.py"] + with stage_selected_files(repository, files) as snapshot: + environment = os.environ.copy() + environment["SOVEREIGNAI_STAGING_ROOT"] = str(snapshot.root) + environment["SOVEREIGNAI_ENTRY_SCRIPT"] = "scripts/appcontainer_user_code_smoke.py" + completed = subprocess.run( + [str(Path(sys.argv[1]).resolve(strict=True))], + cwd=repository, + env=environment, + check=False, + ) + return completed.returncode + + +if __name__ == "__main__": + raise SystemExit(main())