所有專案
06SentinelBoxAI Agent 沙盒2026開源

隔離 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
sentinelbox — strict

$ 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

示意重播:訊息格式與語意文字取自原始碼與 mappings/syscall_feedback.json;PID、耗時等數值省略。
個 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、哪條規則,於是能回答「為什麼」與「怎麼改」,形成「執行 → 失敗 → 回饋 → 修正」的閉環。

  • NOTIFYsocket(AF_INET, …)
    nc: socket: Operation not permitted

    安全違規:嘗試在 profile「strict」內建立 socket(family=2)。

    HINT 改用沙盒外預先準備好的資料;本 profile 禁止任何網路連線。

  • KILLmount("proc", …)
    Killed            exit 137

    動作拒絕:禁止變更檔案系統或 namespace(syscall=mount)。

    HINT 沙盒內 rootfs 為唯讀 overlay;不需要也不允許 remount。

  • KILLmemory.max exceeded
    Killed            exit 137

    資源耗盡:行程超出記憶體限制被核心強制終止 (Cgroup memory.max violation)

    HINT 改進方案:檢查是否有記憶體洩漏,或在 profile 中提高 memory.max 上限。

02

四個獨立行程,各自用最合適的語言守一層。

C 負責貼近核心的隔離、Rust 負責安全決策與稽核、TypeScript 把結果交給 AI,Web 端負責可觀測性。滑過或點選元件查看職責,或依序播放一次完整的執行流程。

呼叫 / 資料流後備路徑隔離邊界
AI 端主機 · 使用者空間可觀測性LINUX 核心SANDBOXuser · pid · mnt · net · uts · ipc · cgrouptarget processexecvp() · uid 0 ↦ host uidOverlayFS rootlower: busyboxupper: tmpfspivot_rootHardeningno_new_privs−13 capsdumpable = 0seccomp filterAI AgentClaude · any MCP clientMCP ServerTypeScript · stdiotranslate_errorsuggest_repairsentinelboxC · orchestratorprofile → cgroup → clone()sentinelbox-monitorRust · security sentinelpoll(notify_fd) → RECVpolicy.lookup(syscall)feedback map → zh/en/hinttelemetry thread · 100 msSQLite · WALexecutionssyscall_eventsresource_samplesBridgeExpress · Socket.ioDashboardReactlive CPU / memoryevents · kill switchnamespaces7 × CLONE_NEW*uid_map · gid_mapseccomp-BPFALLOW · ERRNO → decided in-kernelNOTIFY · KILL → listener fdcgroup v2memory.max · cpu.maxpids.max · memory.eventsprocfs/proc/stat · /proc/meminfofallback, no delegation1fork+exec2clone()3notify_fd4syscall5USER_NOTIF ⇄ reply6audit7100 ms7fallback[SEMANTIC] + HINT8MCP · stdio8poll9Socket.io9

← 左右滑動看完整圖 →

03

一次執行,從 clone() 到 rmdir。

順序本身就是安全設計:先寫 uid_map 才讓 child 繼續、先降權再安裝 seccomp、最後才 execvp。每一步都對應到原始碼中的實際呼叫。

  1. 01準備

    1. 父行程

      載入 profile,建立 sentinelbox.<pid> cgroup 並寫入 memory.max、cpu.max、pids.max。

    2. 父行程 → Monitor

      建立 socketpair,fork + exec 啟動 monitor;socket dup2 到 fd 3,cgroup 路徑與 profile 以環境變數傳入。

    3. 父行程 → 沙盒

      clone() 一次建立 user、pid、mount、net、uts、ipc、cgroup 七個 namespace。

    4. 父行程 → 核心

      父行程寫 setgroups=deny、uid_map、gid_map,並把 child 寫入 cgroup.procs。

    5. 父行程 → 沙盒

      透過 sync pipe 傳出 child 的宿主 PID,解除 child 的等待。

  2. 02隔離

    1. 沙盒

      成為沙盒內 uid 0(對應宿主非特權使用者),並禁止 core dump。

    2. 沙盒 → 核心

      掛 tmpfs 與 overlay、pivot_root 後卸載舊根,再掛 /proc(巢狀容器時改用 rbind)。

    3. 沙盒

      設定 hostname、PR_SET_NO_NEW_PRIVS,並從 effective / permitted / inheritable / bounding 移除 13 項 capability。

    4. 沙盒 → 核心

      依 profile 安裝 seccomp filter,取得 USER_NOTIF listener fd。

    5. 沙盒 → Monitor

      以 sendmsg(SCM_RIGHTS) 把 notify_fd 與「PID:<pid>|CMD:<argv>」交給 monitor。

    6. 沙盒

      execvp() 執行目標程式,之後的 syscall 都受 filter 管控。

  3. 03監督

    1. 核心 → Monitor

      命中 NOTIFY / KILL 規則的 syscall 被暫停,通知送到 monitor。

    2. Monitor → 核心

      查 policy 後回覆:放行(CONTINUE)或 −EPERM;KILL 規則再送 SIGKILL,並寫入稽核紀錄。

    3. Monitor

      telemetry 執行緒每 100 ms 讀 memory.current 與 cpu.stat 寫入 resource_samples。

  4. 04收尾

    1. 核心 → 父行程

      child 結束,wait4() 一次取得結束狀態與 rusage(作為 cgroup 讀不到時的後備數據)。

    2. 核心 → Monitor

      notify_fd 回報 POLLHUP,monitor 記錄結束,並從 memory.events 判斷是否 OOM。

    3. 父行程

      等待 monitor 結束、輸出資源摘要,最後刪除 cgroup。

04

同一個 syscall,在不同 profile 會走不同的路。

選一個 profile 與系統呼叫,看它在哪裡被決定、程式實際收到什麼、以及 AI 會得到哪一段回饋。規則與文字直接取自 profiles/*.json 與 mappings/syscall_feedback.json。

Profile
系統呼叫

色點代表此 profile 下的動作: ALLOWERRNONOTIFYKILL

socket()strictNOTIFY

明列於 profile 的 syscall_rules

① 核心內的 seccomp-BPF filter
比對到規則,syscall 被暫停並交給 USER_NOTIF listener。
② Rust monitor 查 policy
policy.lookup("socket") → NOTIFY;寫入 syscall_events(signal_name = EPERM)。
③ 目標程式收到的結果
收到 −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
NOTIFY × 6KILL × 19ERRNO × 3

profiles/strict.json · default_action: ALLOW

datascience

放寬記憶體與 CPU;攔截 connect / bind,允許本地 socket 通訊。

memory.max
1 GiB
cpu.max
2 CPU
pids.max
128
NOTIFY × 2KILL × 13ERRNO × 1

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
NOTIFY × 4KILL × 13ERRNO × 1

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 trying
to access a restricted resource. The violation occurred
during 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 repair
strategy: The code attempts to access the network. In the
'strict' profile, networking is disabled.
Repair Suggestion: Replace network requests with local mock
data 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

而非 chroot

chroot 可被逃逸;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 call

notify_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. 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. 2

    啟動監控面板

    Bridge 跑在 3001、Dashboard 跑在 3000,自動連上 sentinelbox.db。

    ./scripts/start_monitoring.sh
    # open http://localhost:3000
  3. 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)