Verify staged files in AppContainer

This commit is contained in:
Hamza Ayed
2026-10-03 12:51:26 +03:00
parent ce70fda360
commit 82bb5189a1
3 changed files with 53 additions and 2 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 يحتاج مسؤولًا؛ محاولة 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).)
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).)
6. لا وصول عام إلى القرص، ولا أوامر مدمرة أو نشر خارجي دون موافقة صريحة. كل أداة لها مخطط مدخلات ومخرجات واختبارات وسجل تدقيق.
### كودكس للبرمجة
@@ -123,6 +123,37 @@ static void SetEnvironmentValue(
block.push_back(L'\0');
}
static bool FilesMatch(const std::wstring& leftPath, const std::wstring& rightPath) {
HANDLE left = CreateFileW(leftPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
HANDLE right = CreateFileW(rightPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (left == INVALID_HANDLE_VALUE || right == INVALID_HANDLE_VALUE) {
if (left != INVALID_HANDLE_VALUE) CloseHandle(left);
if (right != INVALID_HANDLE_VALUE) CloseHandle(right);
return false;
}
LARGE_INTEGER leftSize{}, rightSize{};
bool matches = GetFileSizeEx(left, &leftSize) && GetFileSizeEx(right, &rightSize) &&
leftSize.QuadPart == rightSize.QuadPart && leftSize.QuadPart >= 0 &&
leftSize.QuadPart <= 1024 * 1024;
std::vector<char> leftBytes(matches ? static_cast<size_t>(leftSize.QuadPart) : 0);
std::vector<char> rightBytes(matches ? static_cast<size_t>(rightSize.QuadPart) : 0);
DWORD leftRead = 0, rightRead = 0;
if (matches && !leftBytes.empty()) {
matches = ReadFile(left, leftBytes.data(), static_cast<DWORD>(leftBytes.size()),
&leftRead, nullptr) &&
ReadFile(right, rightBytes.data(), static_cast<DWORD>(rightBytes.size()),
&rightRead, nullptr) &&
leftRead == static_cast<DWORD>(leftBytes.size()) &&
rightRead == static_cast<DWORD>(rightBytes.size()) &&
leftBytes == rightBytes;
}
CloseHandle(left);
CloseHandle(right);
return matches;
}
static DWORD RunContained(
PSID appContainerSid,
const std::wstring& command,
@@ -274,11 +305,15 @@ 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";
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);
std::wstring curlPath = sandboxWorkspacePath + L"\\curl.exe";
bool curlCopied = workspaceFolderReady &&
CopyFileW(L"C:\\Windows\\System32\\curl.exe", curlPath.c_str(), FALSE);
@@ -304,6 +339,14 @@ int wmain() {
? RunContained(appContainerSid, L"echo contained>" + allowedWritePath,
environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
std::wstring stagedFileUrl = L"file:///";
for (wchar_t ch : stagedInputPath) {
stagedFileUrl += ch == L'\\' ? L'/' : ch;
}
DWORD stagedReadResult = jobReady && stagedInputCopied && curlCopied
? RunContained(appContainerSid, QuoteArg(curlPath) + L" --fail --silent " +
stagedFileUrl + L" -o " + stagedCopyPath, environment.data(), cwd, job)
: ERROR_INVALID_HANDLE;
DWORD curlVersionResult = jobReady && curlCopied
? RunContained(appContainerSid, QuoteArg(curlPath) + L" --version",
environment.data(), cwd, job)
@@ -336,11 +379,15 @@ int wmain() {
&count, nullptr) && count >= 9 && memcmp(contents, "contained", 9) == 0;
CloseHandle(allowedOutput);
}
bool stagedInputRoundTripMatches = stagedInputCopied && FilesMatch(
L"README.md", stagedCopyPath);
if (job) CloseHandle(job);
if (appContainerFolder) CoTaskMemFree(appContainerFolder);
FreeSid(appContainerSid);
DeleteFileW(curlPath.c_str());
DeleteFileW(stagedInputPath.c_str());
DeleteFileW(stagedCopyPath.c_str());
DeleteAppContainerProfile(profileName.c_str());
DeleteFileW(secretPath.c_str());
DeleteFileW(writePath.c_str());
@@ -354,11 +401,15 @@ 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",
stagedReadResult, stagedInputCopied ? L"true" : L"false",
stagedInputRoundTripMatches ? 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 ||
!curlCopied || curlVersionResult != 0 || curlNetworkResult == 0) return 20;
return 0;
}
@@ -15,7 +15,7 @@ $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
$compile = 'call "{0}" && cl.exe /nologo /EHsc /W4 /MT /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"