📌 الدليل الرئيسي: WebAssembly في الإنتاج
agents IA الحديثة تكتب كودًا، تُجَمِّعه وتُنَفِّذه — هذا ما يمنحها استقلاليّتها. هذا أيضًا ما يجعلها سطح الهجوم الأخطر في بنية تحتية: prompt سيّء التحكّم، تسريب مفتاح API، script يمسح /home، فيكتشف المُشَغِّل الحادث بعد فوات الأوان. نموذج القدرات في WebAssembly + WASI 0.2 يُصَحِّح هذا هيكليًّا: مكوّن مُنَفَّذ في Wasmtime لا يملك افتراضيًّا أيّ ملفّ، أيّ socket، أيّ ساعة، ويُمنَح صراحة ما هو ضروري.
يبني هذا الدليل runner Rust يُنَفِّذ كود مستخدم في خليّة Wasmtime بقدرات صارمة: نظام ملفّات قراءة-كتابة محدود بمجلّد scratch، لا شبكة، ساعة ثابتة، حدّ ذاكرة صارم، timeout عبر epoch interruption. النتيجة: نظام فرعي قابل لإعادة الاستعمال، قابل للنشر في الإنتاج.
الخطوة 1 — تعريف نموذج التهديد
قبل أيّ سطر، نُحَدِّد ما يجب على sandbox منعه. أربع عائلات مخاطر تهيمن عند تنفيذ كود من LLM أو مستخدم ثالث.
الهروب نحو النظام المضيف. الكود قد يُحاول قراءة /etc/passwd، ~/.ssh/id_rsa، متغيّرات بيئة بـ tokens. WebAssembly يمنع هيكليًّا syscalls غير المُوَصَّلة: لا واجهة open تتجاوز المضيف.
تسريب شبكي. الكود قد يحاول POST بيانات إلى خادم مهاجم. دون منح wasi-sockets أو wasi-http outgoing، لا اتّصال خارج ممكن.
منع خدمة. الكود قد يُخَصِّص كلّ الذاكرة أو يدور بلا نهاية. حدود الذاكرة والمقاطعة بـ epoch تحلّ هذا.
الديمومة والتسلسل. الكود قد يكتب ملفًّا يقرأه script آخر لاحقًا. نُعَوِّض باستعمال preopens للقراءة فقط أو مجلّدات scratch تُرمى بعد كلّ تنفيذ.
الخطوة 2 — تحضير مشروع المضيف Rust
cargo new --bin agent-sandbox
cd agent-sandbox
cargo add wasmtime@44 wasmtime-wasi@44 anyhow uuid sha2 hex
cargo add tokio --features rt-multi-thread,macros
cargo add uuid --features v4
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
الخطوة 3 — تفعيل epoch interruption في engine
epoch interruption هو الطريق الموصى به من Wasmtime لقطع كود مستخدم: يُضيف نحو 10% overhead ويبقى 2 إلى 3 مرّات أسرع من fuel (fuel يفرض عدّادًا في كلّ تعليمة WebAssembly، epoch يلاحظ عدّادًا شموليًّا نادر التعديل فقط).
use anyhow::Result;
use wasmtime::component::{Component, Linker, ResourceTable};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::p2::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
struct AppState { ctx: WasiCtx, table: ResourceTable, limits: StoreLimits }
impl WasiView for AppState {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView { ctx: &mut self.ctx, table: &mut self.table }
}
}
fn make_engine() -> Result {
let mut cfg = Config::new();
cfg.wasm_component_model(true)
.epoch_interruption(true)
.consume_fuel(false);
Ok(Engine::new(&cfg)?)
}
الـ engine يحمل العدّاد الشمولي لـ epoch. thread خلفي يُزَيِّده دوريًّا، وكلّ Store يُعَرِّف deadline نسبية. حين تُبلَغ deadline، كود المستخدم يَتراب نظيفًا، دون قتل المضيف.
الخطوة 4 — تهيئة سياق WASI بقدرات صارمة
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
fn make_wasi(scratch_dir: &str) -> Result<(WasiCtx, MemoryOutputPipe, MemoryOutputPipe)> {
let dir = std::fs::canonicalize(scratch_dir)?;
std::fs::create_dir_all(&dir)?;
let stdout = MemoryOutputPipe::new(64 * 1024);
let stderr = MemoryOutputPipe::new(64 * 1024);
let ctx = WasiCtxBuilder::new()
.preopened_dir(&dir, "/scratch", DirPerms::all(), FilePerms::all())?
.env("RUN_ID", &uuid::Uuid::new_v4().to_string())
// لا inherit_env(): لا متغيّر مضيف مكشوف
// لا inherit_network(): لا socket خارج
.stdout(stdout.clone())
.stderr(stderr.clone())
.build();
Ok((ctx, stdout, stderr))
}
preopen /scratch هو السطح الوحيد filesystem مرئي. DirPerms::all() يسمح بالقراءة والكتابة والقائمة بالداخل. غياب inherit_env() أساسي: المكوّن لا يرى متغيّرات المضيف. إن أطلق المُشَغِّل الـ binary مع AWS_SECRET_ACCESS_KEY، كود الـ agent لا يستطيع قراءتها — غير موجودة بالنسبة له.
الخطوة 5 — حدود ذاكرة وfuel كحزام أمان ثانٍ
use wasmtime::{StoreLimits, StoreLimitsBuilder};
fn make_store(engine: &Engine, ctx: WasiCtx, max_memory_mb: usize) -> Store {
let limits = StoreLimitsBuilder::new()
.memory_size(max_memory_mb * 1024 * 1024)
.instances(1)
.tables(1)
.build();
let state = AppState { ctx, table: ResourceTable::new(), limits };
let mut store = Store::new(engine, state);
store.limiter(|s| &mut s.limits);
store.set_epoch_deadline(1); // trap عند tick epoch المتجاوز
store
}
حدّ الذاكرة يسقف linear memory للمكوّن. حدّ instances يمنع مكوّنًا عدائيًّا من إنشاء مئات sous-modules.
الخطوة 6 — تشغيل tick epoch في الخلفية
use std::sync::Arc;
use std::time::Duration;
fn spawn_epoch_ticker(engine: Arc, tick: Duration) {
std::thread::spawn(move || loop {
std::thread::sleep(tick);
engine.increment_epoch();
});
}
بـ tick 100 ms وset_epoch_deadline(50)، المكوّن له 5 ثوانٍ تنفيذ كحدّ أقصى. ما فوق ذلك، Wasmtime يتراب نظيفًا.
الخطوة 7 — تنفيذ مهمّة مستخدم والتقاط الخرج
use std::sync::Arc;
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
async fn run_user_task(engine: Arc, wasm_path: &str, scratch: &str)
-> Result<(String, String)>
{
let component = Component::from_file(&engine, wasm_path)?;
let mut linker = Linker::::new(&engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
let (ctx, stdout, stderr) = make_wasi(scratch)?;
let mut store = make_store(&engine, ctx, /*max_memory_mb=*/64);
let cmd = wasmtime_wasi::p2::bindings::sync::Command::instantiate(
&mut store, &component, &linker)?;
let outcome = cmd.wasi_cli_run().call_run(&mut store);
let out = String::from_utf8_lossy(&stdout.contents()).to_string();
let err = String::from_utf8_lossy(&stderr.contents()).to_string();
match outcome {
Ok(Ok(())) => Ok((out, err)),
Ok(Err(())) => Err(anyhow::anyhow!("task exited with error: {}", err)),
Err(e) => Err(anyhow::anyhow!("trap: {} | stderr: {}", e, err)),
}
}
لكلّ تنفيذ، نُنشئ Store جديدًا — وحدة العزل. لا حالة مشتركة بين تنفيذين. في trap (timeout، ذاكرة متجاوزة، panic)، الدالّة تُرجع خطأ مُهَيكَل — لا انهيار للمضيف.
الخطوة 8 — Audit log غير قابل للتعديل للتنفيذات
use sha2::{Digest, Sha256};
fn module_fingerprint(path: &str) -> Result {
let bytes = std::fs::read(path)?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
Ok(hex::encode(hasher.finalize()))
}
السجلّ المُهَيكَل يحوي: timestamp، run_id، hash module، ذاكرة pic، مدّة، رمز خروج، أوّل أسطر stdout/stderr. يكفي لإعادة بناء سلسلة التنفيذ إن كُشف سلوك مريب.
الخطوة 9 — توسيع القدرات بحذر
السيناريو الأدنى (filesystem + epoch + ذاكرة) يُغَطّي معظم الحالات. للأجنتر التي يجب أن تستدعي API خارجي، نُوَسِّع بحذر.
HTTP خارج عبر wasi-http. يمكن وصل HttpClient لا يُوَجِّه سوى إلى whitelist domains. crate wasmtime-wasi-http يكشف الواجهة؛ في المضيف، نعترض كلّ طلب ونحجب ما خارج القائمة.
متغيّرات بيئة صريحة. إن وجب على الـ agent رؤية مفتاح API، نُمَرِّره عبر env() اسميًّا — لا عبر inherit_env().
عدّة preopens للقراءة فقط. مجلّد /config بـ DirPerms::READ ومجلّد /output للكتابة يفصلان الإعداد عن الكتابة، ممّا يمنع المكوّن من إعادة كتابة إعداده.
الخطوة 9bis — توقيع والتحقّق من modules قبل التنفيذ
sandbox مُهَيَّأ جيّدًا لا يمنع تحميل مكوّن سيّء. إن استولى المهاجم على pipeline البناء، قد يستبدل الـ binary قبل وصوله إلى المضيف. الردّ توقيع modules عند خروج build والتحقّق قبل instantiation.
الطريق القياسي Sigstore (cosign)، يُوَقِّع بمفتاح عابر مُوَلَّد على الطاير وينشر البصمة في سجلّ شفّاف (Rekor). في مضيف Rust، نتحقّق قبل Component::from_file.
// pseudo-flux
let expected = "sha256:abc123..."; // البصمة المنتظرة، مُوَزَّعة عبر قناة منفصلة
let got = module_fingerprint(wasm_path)?;
if got != expected { return Err(anyhow::anyhow!("module hash mismatch")); }
let component = Component::from_file(&engine, wasm_path)?;
الخطوة 9ter — تحديد عدد التنفيذات المتزامنة
على مضيف يخدم عدّة agents، نُقَيِّد parallelism. semaphore Tokio (tokio::sync::Semaphore) بـ N permits يحدّ المهامّ المتزامنة؛ الـ agents ما فوق تنتظر في القائمة. يمنع agent جشعًا من حجب الآخرين.
نُرافق المجموع بـ dashboard بسيط: عدد المهامّ الجارية، مدّة median، p95، نسبة trap للـ timeout vs الذاكرة. للهياكل الأكثر تقدّمًا (queue مهامّ دائمة، retries، scheduling بأولوية)، نستند إلى orchestrateur مخصَّص — Temporal، Inngest، أو MCP server مخصَّص.
الخطوة 10 — أخطاء شائعة
| العَرَض | السبب | الحلّ |
|---|---|---|
| مكوّن trap فورًا | set_epoch_deadline غير مُستدعى |
استدعِ دائمًا قبل call_run |
| لا مقاطعة رغم timeout | Ticker غائب أو بطيء | تحقّق من spawn_epoch_ticker نشط |
| مكوّن يقرأ خارج scratch | preopen ضخم | تحقّق من مسار preopened_dir |
| التقاط stdout فارغ | inherit_stdio() مُفَعَّل خطأً |
استبدل بـ MemoryOutputPipe |
| خطأ شبكي في المكوّن | لا outbound مُوَصَّل (طبيعي) | أضف HttpClient whitelist إن لزم |
| ذاكرة متجاوزة غير مكشوفة | store.limiter غير مُهَيَّأ |
طَبِّق StoreLimitsBuilder |
المُشَغِّل الذي يقود sandbox عليه مسؤولية تشغيلية: فرز التنبيهات، إبقاء نسخة Wasmtime محدَّثة (إنذارات أبريل 2026 على LTS تُذَكِّر بأنّ runtime نفسه يُدَقَّق كـ binary حرج)، واختبار القدرات دوريًّا بتشغيل سيناريو هجوم صغير (مكوّن يحاول الكتابة خارج scratch، الدوران بلا نهاية، تخصيص 8 جيغا) للتحقّق من أنّ كلّ حدّ يُشَغِّل الـ trap المنتظر.
للتعمّق
هذه sandbox تعزل مكوّنًا مُجَمَّعًا. لتنفيذ كود Python أو JavaScript مُوَلَّد من الـ agent، نُجَمِّع المُفَسِّر ذاته في مكوّن (componentize-py لـ CPython، ComponentizeJS الذي يُضَمِّن محرّك StarlingMonkey لـ JavaScript) ثم نُحَمِّله في نفس الخليّة.
- WASI وخوادم WebAssembly مع Wasmtime
- نشر WebAssembly Rust على Cloudflare Workers
- الدليل الرئيسي WebAssembly في الإنتاج
مصادر رسمية
docs.wasmtime.dev— Interrupting Executiondocs.wasmtime.dev— Configdocs.wasmtime.dev—wasmtime_wasiWasiCtx والقدراتgithub.com/bytecodealliance/wasmtime— crates/wasicomponent-model.bytecodealliance.orggithub.com/bytecodealliance/componentize-pywasi.dev/interfaces
WebAssembly يُحَوِّل تنفيذ كود مستخدم أو مُوَلَّد من مشكلة ثقة إلى مشكلة إعداد صريح. الخطوات الثماني أعلاه تكفي لإقامة خليّة تعزل هيكليًّا filesystem، شبكة، ذاكرة، وزمن CPU. أكثر استثمار مُربح عند نشر agents IA في الإنتاج.