隔離 AI 寫的程式碼,也告訴它為什麼失敗。
SentinelBox 以 Linux namespaces、OverlayFS、seccomp USER_NOTIF 與 cgroup v2 隔離執行。每一次被攔下的系統呼叫,都會由 Rust 監控器轉成 AI 可據以修正的語意回饋,並透過 Model Context Protocol 提供給 Agent。
- Model Context Protocol
- Rootless · Linux 5.15+
- C · Rust · TypeScript
$ bash scripts/run.sh -- /bin/sh -c "nc -w1 example.com 80"
[sentinelbox] profile=strict rootfs=./rootfs/busybox
[sentinelbox] clone child: user·pid·mnt·net·uts·ipc·cgroup
[monitor] policy loaded rules=28 db=WAL
[sentinelbox] seccomp listener ready → notify_fd via SCM_RIGHTS
socket(AF_INET, SOCK_STREAM, 0) ⟶ USER_NOTIF
[SEMANTIC] 安全違規:嘗試在 profile「strict」內建立 socket(family=2)。
HINT → 改用沙盒外預先準備好的資料;本 profile 禁止任何網路連線。
nc: socket: Operation not permitted
[monitor] summary: mem_peak=… KiB cpu_total=… ms
- 個 namespace,一次 clone() 建立
- 7
- 個 namespace,一次 clone() 建立
- 種 seccomp 動作:ALLOW · ERRNO · NOTIFY · KILL
- 4
- 種 seccomp 動作:ALLOW · ERRNO · NOTIFY · KILL
- cgroup v2 資源取樣週期
- 100 ms
- cgroup v2 資源取樣週期
- 項高風險 capability 移除(含 bounding set)
- 13
- 項高風險 capability 移除(含 bounding set)
01
傳統沙盒只回一個錯誤碼,AI 只能瞎猜。
「Operation not permitted」或「Killed」對 LLM 來說幾乎沒有資訊量:是程式寫錯、權限不足,還是政策禁止?SentinelBox 在攔截的當下就知道是哪個 syscall、哪條規則,於是能回答「為什麼」與「怎麼改」,形成「執行 → 失敗 → 回饋 → 修正」的閉環。
- NOTIFY
socket(AF_INET, …)nc: socket: Operation not permitted
安全違規:嘗試在 profile「strict」內建立 socket(family=2)。
HINT 改用沙盒外預先準備好的資料;本 profile 禁止任何網路連線。
- KILL
mount("proc", …)Killed exit 137
動作拒絕:禁止變更檔案系統或 namespace(syscall=mount)。
HINT 沙盒內 rootfs 為唯讀 overlay;不需要也不允許 remount。
- KILL
memory.max exceededKilled exit 137
資源耗盡:行程超出記憶體限制被核心強制終止 (Cgroup memory.max violation)
HINT 改進方案:檢查是否有記憶體洩漏,或在 profile 中提高 memory.max 上限。
02
四個獨立行程,各自用最合適的語言守一層。
C 負責貼近核心的隔離、Rust 負責安全決策與稽核、TypeScript 把結果交給 AI,Web 端負責可觀測性。滑過或點選元件查看職責,或依序播放一次完整的執行流程。
← 左右滑動看完整圖 →
03
一次執行,從 clone() 到 rmdir。
順序本身就是安全設計:先寫 uid_map 才讓 child 繼續、先降權再安裝 seccomp、最後才 execvp。每一步都對應到原始碼中的實際呼叫。
01準備
父行程
載入 profile,建立 sentinelbox.<pid> cgroup 並寫入 memory.max、cpu.max、pids.max。
父行程 → Monitor
建立 socketpair,fork + exec 啟動 monitor;socket dup2 到 fd 3,cgroup 路徑與 profile 以環境變數傳入。
父行程 → 沙盒
clone() 一次建立 user、pid、mount、net、uts、ipc、cgroup 七個 namespace。
父行程 → 核心
父行程寫 setgroups=deny、uid_map、gid_map,並把 child 寫入 cgroup.procs。
父行程 → 沙盒
透過 sync pipe 傳出 child 的宿主 PID,解除 child 的等待。
02隔離
沙盒
成為沙盒內 uid 0(對應宿主非特權使用者),並禁止 core dump。
沙盒 → 核心
掛 tmpfs 與 overlay、pivot_root 後卸載舊根,再掛 /proc(巢狀容器時改用 rbind)。
沙盒
設定 hostname、PR_SET_NO_NEW_PRIVS,並從 effective / permitted / inheritable / bounding 移除 13 項 capability。
沙盒 → 核心
依 profile 安裝 seccomp filter,取得 USER_NOTIF listener fd。
沙盒 → Monitor
以 sendmsg(SCM_RIGHTS) 把 notify_fd 與「PID:<pid>|CMD:<argv>」交給 monitor。
沙盒
execvp() 執行目標程式,之後的 syscall 都受 filter 管控。
03監督
核心 → Monitor
命中 NOTIFY / KILL 規則的 syscall 被暫停,通知送到 monitor。
Monitor → 核心
查 policy 後回覆:放行(CONTINUE)或 −EPERM;KILL 規則再送 SIGKILL,並寫入稽核紀錄。
Monitor
telemetry 執行緒每 100 ms 讀 memory.current 與 cpu.stat 寫入 resource_samples。
04收尾
核心 → 父行程
child 結束,wait4() 一次取得結束狀態與 rusage(作為 cgroup 讀不到時的後備數據)。
核心 → Monitor
notify_fd 回報 POLLHUP,monitor 記錄結束,並從 memory.events 判斷是否 OOM。
父行程
等待 monitor 結束、輸出資源摘要,最後刪除 cgroup。
04
同一個 syscall,在不同 profile 會走不同的路。
選一個 profile 與系統呼叫,看它在哪裡被決定、程式實際收到什麼、以及 AI 會得到哪一段回饋。規則與文字直接取自 profiles/*.json 與 mappings/syscall_feedback.json。
色點代表此 profile 下的動作: ALLOWERRNONOTIFYKILL
socket()strictNOTIFY明列於 profile 的 syscall_rules
policy.lookup("socket") → NOTIFY;寫入 syscall_events(signal_name = EPERM)。AI 收到的語意回饋
[SEMANTIC] 安全違規:嘗試在 profile「strict」內建立 socket(family=2)。
HINT → 改用沙盒外預先準備好的資料;本 profile 禁止任何網路連線。
規則與訊息直接讀取 GitHub 上 mcp-sentinel-box 的 main 分支(每小時更新)。 profiles/ · syscall_feedback.json
05
六層機制疊在一起,任何一層失守都還有下一層。
隔離不靠單一技術。即使某個 syscall 規則漏列,沙盒仍在獨立的 namespace、唯讀的根目錄、被剝除的 capability 與 cgroup 上限之內。
- L1
Namespaces
- 一次 clone() 建立 user、pid、mount、net、uts、ipc、cgroup
- 沙盒內 uid 0 = 宿主非特權使用者(rootless)
- 先寫 setgroups=deny 再寫 gid_map
core/src/sandbox.c
- L2
檔案系統
- OverlayFS:busybox 唯讀 lower + tmpfs upper,寫入隨沙盒消失
- pivot_root + MNT_DETACH 取代 chroot,杜絕逃逸
- 根掛載設為 private,掛載動作不外洩到宿主
core/src/overlayfs.c
- L3
權限
- PR_SET_NO_NEW_PRIVS:setuid 程式也拿不到額外權限
- 移除 13 項高風險 capability,包含 bounding set
- PR_SET_DUMPABLE = 0,避免 core dump 洩漏記憶體
core/src/capability.c
- L4
系統呼叫
- seccomp-BPF + SECCOMP_RET_USER_NOTIF,不用 ptrace
- ALLOW 規則優先加入,讓高頻 syscall 早點放行
- KILL 編成 NOTIFY:先記錄、再終止
core/src/seccomp.c
- L5
資源
- cgroup v2:memory.max、cpu.max、pids.max(擋 fork bomb)
- 每次執行獨立 cgroup,結束即移除
- memory.events 的 oom_kill 轉成 OOM 語意事件
core/src/cgroup.c
- L6
稽核
- 每一筆攔截都寫入 SQLite,含 errno 符號與語意文字
- WAL + synchronous=NORMAL + busy_timeout 5 s
- 執行、事件、取樣三張表,皆以毫秒計時
monitor/src/db.rs
三個內建 profile
Profile 是 C 核心與 Rust monitor 共用的同一份 JSON:前者據以安裝 seccomp filter 與 cgroup 上限,後者據以做出決策。
strict
預設預設。禁止建立網路連線,阻擋核心安全相關 syscall,所有違規都通報 monitor。
- memory.max
- 128 MiB
- cpu.max
- 0.5 CPU
- pids.max
- 32
profiles/strict.json · default_action: ALLOW
datascience
放寬記憶體與 CPU;攔截 connect / bind,允許本地 socket 通訊。
- memory.max
- 1 GiB
- cpu.max
- 2 CPU
- pids.max
- 128
profiles/datascience.json · default_action: ALLOW
web
給 Web Agent:policy 允許出站 socket,攔截 bind / listen / accept 等對內服務。
- memory.max
- 256 MiB
- cpu.max
- 1 CPU
- pids.max
- 64
profiles/web.json · default_action: ALLOW
規則與訊息直接讀取 GitHub 上 mcp-sentinel-box 的 main 分支(每小時更新)。 profiles/ · syscall_feedback.json
06
每一次攔截與資源曲線,都留下可查詢的紀錄。
稽核資料庫採 SQLite WAL 模式:monitor 持續寫入的同時,Bridge、Dashboard 與 MCP 端都能並行讀取。數值一律存原始值,時間戳統一為毫秒,換算交給前端。
syscall_events
每一次 NOTIFY / KILL 攔截
- exec_idFK
- tsms
- syscall_nrx86_64
- syscall_name
- actionNOTIFY | KILL
- errno
- signal_nameEPERM | SIGKILL
- semantic_msg
- remediationhint
executions
每一次沙盒執行
- idPK
- profile
- pidhost
- commandargv
- start_tsms
- end_tsNULL = running
- status
resource_samples
每 100 ms 一筆取樣
- exec_idFK
- tsms
- mem_bytesmemory.current
- cpu_useccpu.stat
- cpu_pctΔ / interval
- 100 ms
- 取樣執行緒預先開好 memory.current 與 cpu.stat,每輪 seek(0) 重讀,避免重複 open。
- cpu_pct
- 以 cpu.stat 的 usage_usec 差值除以實際經過時間計算;多核心時可超過 100%。
- fallback
- cgroup 無法委派時改讀 /proc/stat、/proc/meminfo;核心端另以 wait4() 的 rusage 補上總用量。
- OOM
- 結束時讀 memory.events 的 oom_kill,若被核心 OOM kill 就補記一筆語意事件。
查詢最近 5 筆被攔截的 syscall
SELECT datetime(e.ts / 1000, 'unixepoch') AS at,
x.profile, e.syscall_name, e.action,
e.signal_name, e.semantic_msg
FROM syscall_events e
JOIN executions x ON x.id = e.exec_id
ORDER BY e.id DESC
LIMIT 5;PRAGMA:journal_mode=WAL、synchronous=NORMAL、busy_timeout=5000。monitor 與取樣執行緒共用同一個連線(Arc<Mutex>),避免多個 writer 互搶。
07
讓任何支援 MCP 的 Agent 直接接上。
MCP 伺服器以 stdio 執行,提供兩個工具。Agent 在執行失敗後呼叫它們,取得原因與修正方向,再改寫程式碼重試,不需要人介入。
translate_error
把底層訊號(signal / errno / syscall / path)翻成 AI 讀得懂的說明。
- signal?
- errno?
- syscall?
- path?
- details?
suggest_repair
依被擋下的 syscall 與當前 profile,給出程式碼的修正方向。
- syscall
- original_code
- profile?
claude_desktop_config.json
{
"mcpServers": {
"sentinelbox": {
"command": "node",
"args": ["/path/to/mcp-sentinel-box/mcp-server/dist/index.js"]
}
}
}→ tools/call translate_error{ "errno": "EPERM", "syscall": "socket" }← Security Violation: Operation not permitted. You are tryingto access a restricted resource. The violation occurredduring the `socket` system call.→ tools/call suggest_repair{ "syscall": "connect", "profile": "strict","original_code": "requests.get(url)" }← Based on the blocked syscall 'connect', here is the repairstrategy: The code attempts to access the network. In the'strict' profile, networking is disabled.Repair Suggestion: Replace network requests with local mockdata or read from a pre-mounted dataset in the workspace.…
08
為什麼這樣做,而不是那樣做。
以下取捨都寫在原始碼的註解裡,也是 SentinelBox 與「包一層 Docker」最大的差別。
SECCOMP_RET_USER_NOTIF
而非 ptrace / SECCOMP_RET_TRACE不必讓 tracer 附加在每個 syscall 上:ALLOW 在核心的 BPF 內就決定,只有命中規則的呼叫才經由一個 fd 交給 monitor。
core/src/seccomp.c
clone(CLONE_NEW* …)
而非 unshare()child 一出生就在全部 namespace 裡,少一次切換,也沒有「先跑起來再隔離」的空窗。
core/src/sandbox.c
pivot_root + MNT_DETACH
而非 chrootchroot 可被逃逸;pivot_root 換掉整個根並卸載舊根,沙盒內不存在回到宿主的路徑。
core/src/overlayfs.c
tmpfs upper layer
而非 disk-backed writes所有寫入都落在記憶體;重置沙盒只要丟掉 tmpfs,不需任何持久化清理。
core/src/overlayfs.c
wait4()
而非 waitpid()一次呼叫同時拿到結束狀態與 rusage(CPU 時間、max RSS),對短命的 AI 程式碼最省成本。
core/src/sandbox.c
poll() → NOTIF_RECV
而非 blocking ioctl若沙盒從未觸發 NOTIFY 就結束,阻塞式 RECV 可能永遠醒不來;先 poll,收到 POLLHUP 即可乾淨退出。
monitor/src/main.rs
sendmsg left unfiltered
而非 blocking every socket callnotify_fd 正是用 sendmsg(SCM_RIGHTS) 傳出去的;攔它會讓 child 被自己的 filter 卡死。攔 socket() / connect() 就足以斷網。
profiles/strict.json
C core + Rust monitor
而非 one process貼近核心的設定用 C 最直接;處理 raw fd 與 ioctl 的決策迴圈交給 Rust 的借用檢查。兩者只靠 fd 繼承與環境變數溝通。
monitor/Cargo.toml
09
三個指令,在自己的 Linux 上跑起來。
在 Docker、VS Code Dev Container 或部分 WSL2 環境中,cgroup 無法委派時會自動降級為整機取樣,沙盒仍可執行;要取得精確的每沙盒限制,請在原生 Linux 上執行 scripts/setup_cgroup.sh。
- 1
取得原始碼並一鍵建置
安裝相依套件、編譯 C core 與 Rust monitor,並建立 busybox rootfs。
git clone https://github.com/Ming874/mcp-sentinel-box.git cd mcp-sentinel-box bash scripts/provision.sh - 2
啟動監控面板
Bridge 跑在 3001、Dashboard 跑在 3000,自動連上 sentinelbox.db。
./scripts/start_monitoring.sh # open http://localhost:3000 - 3
在沙盒裡執行程式碼
另開終端機;profile 以環境變數切換。
bash scripts/run.sh -- /bin/sh -c "echo hello from sandbox" bash scripts/run.sh -- /bin/sh -c "nc -w1 example.com 80" SENTINELBOX_PROFILE=datascience bash scripts/run.sh -- /bin/sh -c "free -m"
實際運作
從攔截網路連線、即時資源圖表到 AI 自我修正的完整示範。
進度與下一步
- Phase 1 · 容器原語:namespaces、pivot_root、OverlayFS、cgroup v2、libcap
- Phase 2 · 安全哨兵:Rust、seccomp USER_NOTIF、SCM_RIGHTS
- Phase 3 · 遙測與資料庫:cgroup v2 取樣、SQLite WAL 稽核
- Phase 4 · MCP 整合:TypeScript SDK、自我修復閉環
- Phase 5 · 管理介面:React、Tailwind、Socket.io
- eBPF probe(aya、execve tracepoint、cgroup_skb);目前 monitor/src/ebpf 為保留骨架
- veth 與出站白名單,讓 web / datascience 的網路政策真正生效
- 從 /proc/<pid>/mem 讀取路徑參數,填入 syscall_events.path
- MCP 工具直接查詢稽核資料庫,並共用 syscall_feedback.json
- x86_64 以外的 syscall 對照表(aarch64)