This page teaches you how to run a program you cannot fully trust inside a sandbox, and how to write the rules for what it may touch. The program we have in mind is an AI agent: software that reads files, runs commands and calls services by itself. You learn with NVIDIA OpenShell, a free, open-source tool made for exactly this. You install it, put a small program in a sandbox, watch every door stay shut, and then open one door, for one program, for reading only. 这个页面教你怎样把一个你不能完全信任的程序放进沙盒里运行,以及怎样写下它可以碰什么的规则。 我们想到的这种程序是 AI 智能体:能自己读文件、执行命令、调用服务的软件。你用 NVIDIA OpenShell 来学, 这是一个专门为此而做的免费开源工具。你安装它,把一个小程序放进沙盒,看着每一扇门都关着,然后只打开一扇门: 只给一个程序,只允许读取。 หน้านี้สอนวิธีรันโปรแกรมที่คุณไว้ใจไม่ได้เต็มที่ไว้ในแซนด์บ็อกซ์ และวิธีเขียนกฎว่ามันแตะอะไรได้บ้าง โปรแกรมแบบที่เรานึกถึงคือ AI agent: ซอฟต์แวร์ที่อ่านไฟล์ รันคำสั่ง และเรียกบริการต่าง ๆ ได้ด้วยตัวเอง คุณเรียนด้วย NVIDIA OpenShell เครื่องมือโอเพนซอร์สที่ใช้ฟรีและสร้างมาเพื่องานนี้โดยเฉพาะ คุณติดตั้งมัน ใส่โปรแกรมเล็ก ๆ ไว้ในแซนด์บ็อกซ์ ดูว่าประตูทุกบานปิดอยู่ แล้วเปิดประตูเพียงบานเดียว ให้โปรแกรมเดียว และให้อ่านได้อย่างเดียว
cd and ls are new
to you, start with the Linux page. If Docker is new, the
Docker Lab comes first.
之前用过一点终端。如果 cd 和 ls 对你来说是新的,先看 Linux 页面。如果没用过 Docker,先做 Docker Lab。
เคยใช้เทอร์มินัลมาบ้าง ถ้า cd กับ ls ยังใหม่สำหรับคุณ ให้เริ่มที่หน้า Linux ถ้ายังไม่เคยใช้ Docker ให้ทำ Docker Lab ก่อน--help.
用开发者自己的话说,OpenShell 是什么。“OpenShell 是为成群的自主 AI 智能体准备的安全、私密的运行时。……
你在一份策略里声明每个智能体可以碰什么,OpenShell 负责执行。”NVIDIA 把它发布在 GitHub 上,采用 Apache-2.0 许可。
代码仓库创建于 2026-02-24。本页使用的是 0.1.2 版,发布于 2026-09-28。
年轻的工具变化很快:如果这里的某条命令在你那里打印出别的内容,请相信你的屏幕和工具自己的 --help。
OpenShell คืออะไร ตามคำของผู้สร้าง “OpenShell คือรันไทม์ที่ปลอดภัยและเป็นส่วนตัวสำหรับกลุ่ม AI agent ที่ทำงานเอง …
คุณประกาศไว้ในนโยบายว่า agent แต่ละตัวแตะอะไรได้ แล้ว OpenShell บังคับใช้ตามนั้น” NVIDIA เผยแพร่มันบน GitHub ภายใต้สัญญาอนุญาต Apache-2.0
ที่เก็บโค้ดถูกสร้างเมื่อ 2026-02-24 หน้านี้ใช้เวอร์ชัน 0.1.2 ซึ่งเผยแพร่เมื่อ 2026-09-28
เครื่องมือที่ยังใหม่เปลี่ยนเร็ว: ถ้าคำสั่งในหน้านี้พิมพ์อย่างอื่นบนเครื่องของคุณ ให้เชื่อหน้าจอของคุณและ --help ของเครื่องมือเอง
Twenty-four words. The first twelve are ideas, like sandbox, policy and deny. The last twelve are parts of the tool, like gateway, image and endpoint. You will meet every one of them in the commands. Learn to read them and to say them first.
二十四个词。前十二个是概念,比如沙盒、策略和拒绝。后十二个是工具的部件,比如网关、镜像和端点。它们每一个都会出现在命令里。先学会读它们、说它们。
ยี่สิบสี่คำ สิบสองคำแรกเป็นแนวคิด เช่น แซนด์บ็อกซ์ นโยบาย และปฏิเสธ สิบสองคำหลังเป็นชิ้นส่วนของเครื่องมือ เช่น เกตเวย์ อิมเมจ และเอนด์พอยต์ คุณจะเจอทุกคำในคำสั่ง เรียนอ่านและพูดคำเหล่านี้ก่อน
Tap 🔊 say it to hear a word. Tap 📖 tell me more for a short explanation, read aloud. The translation on each card follows the 中文 and ไทย buttons. 点 🔊 say it 听这个词。点 📖 tell me more 听一段简短的解释。卡片上的翻译跟随“中文”和“ไทย”按钮。 แตะ 🔊 say it เพื่อฟังคำ แตะ 📖 tell me more เพื่อฟังคำอธิบายสั้น ๆ คำแปลบนการ์ดเปลี่ยนตามปุ่ม 中文 และ ไทย
A video of about five minutes. It goes through the whole lab: the three pieces, the file fence, the two checks on every request, the key that the program never sees, and the surprise on our laptop. Watch it before Part 4, so that you know where the fourteen steps are going. A computer voice reads it. Every line of terminal text in it comes from our own run.
一段约五分钟的视频。它把整个实验走一遍:三个部件、文件围栏、对每个请求的两次检查、程序永远看不到的那把钥匙,以及我们笔记本上的那个意外。在做第 4 部分之前先看,这样你就知道那十四个步骤要走向哪里。配音是计算机。视频里的每一行终端文字都来自我们自己的运行。
วิดีโอประมาณห้านาที มันพาไปดูห้องทดลองทั้งหมด: สามชิ้นส่วน รั้วของไฟล์ การตรวจสองครั้งกับทุกคำขอ กุญแจที่โปรแกรมไม่เคยเห็น และเรื่องไม่คาดคิดบนแล็ปท็อปของเรา ดูก่อนทำส่วนที่ 4 คุณจะได้รู้ว่าสิบสี่ขั้นนั้นกำลังไปทางไหน เสียงเป็นคอมพิวเตอร์ ข้อความเทอร์มินัลทุกบรรทัดในวิดีโอมาจากการรันของเราเอง
Watch it once now. Watch it again after Part 4, and you will know every line in it. 现在看一遍。做完第 4 部分后再看一遍,你会认得里面的每一行。 ดูหนึ่งรอบตอนนี้ ดูอีกรอบหลังส่วนที่ 4 แล้วคุณจะรู้จักทุกบรรทัดในนั้น
Six machines. Each one shows one idea from Part 4, and runs here in the page. The lines of terminal text inside them are not invented: the machines read them out of the green boxes in Part 4, which hold what our own computer printed. Where a machine shows something we did not run, it says so. 六台机器。每一台展示第 4 部分里的一个概念,并且就在这个页面里运行。机器里出现的终端文字不是编出来的: 它们是从第 4 部分的绿色方框里读出来的,那些方框里是我们自己的电脑打印的内容。如果机器显示的是我们没有运行过的情况,它会说明。 เครื่องหกเครื่อง แต่ละเครื่องแสดงแนวคิดหนึ่งอย่างจากส่วนที่ 4 และทำงานอยู่ในหน้านี้ ข้อความเทอร์มินัลในเครื่องไม่ได้แต่งขึ้น: เครื่องอ่านมันมาจากกล่องสีเขียวในส่วนที่ 4 ซึ่งเก็บสิ่งที่คอมพิวเตอร์ของเราพิมพ์จริง ถ้าเครื่องแสดงสิ่งที่เราไม่ได้รัน มันจะบอกไว้
OpenShell has three main pieces. The gateway is the control room, and it keeps the policies and the keys. Each sandbox has a supervisor, which is trusted, and a walled-off space for your program, which is not. The program has no way out of its own. Every request goes to the supervisor, and the supervisor decides.
OpenShell 有三个主要部件。网关是控制室,保管策略和钥匙。每个沙盒有一个受信任的监督程序,以及一块用墙围起来、给你的程序用的空间,而你的程序是不受信任的。程序自己没有出路。每个请求都先到监督程序那里,由监督程序决定。
OpenShell มีชิ้นส่วนหลักสามชิ้น เกตเวย์คือห้องควบคุม และเก็บนโยบายกับกุญแจ แซนด์บ็อกซ์แต่ละอันมีซูเปอร์ไวเซอร์ซึ่งเป็นฝ่ายที่ไว้ใจได้ และมีพื้นที่ล้อมกำแพงสำหรับโปรแกรมของคุณซึ่งไม่ถูกไว้ใจ โปรแกรมไม่มีทางออกของตัวเอง ทุกคำขอไปถึงซูเปอร์ไวเซอร์ก่อน แล้วซูเปอร์ไวเซอร์เป็นผู้ตัดสิน
Tap a piece. 点一个部件。แตะชิ้นส่วนหนึ่งชิ้น
Fourteen steps on your own computer. Each step is one small file. The dark box shows the file. The green box under it shows what the file printed when we ran it. A line that starts with a dollar sign is a command, and the lines after it are the answer. Your computer should print nearly the same, apart from times and long numbers.
在你自己的电脑上做十四步。每一步是一个小文件。深色的方框显示文件。它下面的绿色方框显示我们运行这个文件时它打印的内容。以美元符号开头的一行是一条命令,后面的几行是回答。除了时间和长串数字之外,你的电脑打印的应该几乎一样。
สิบสี่ขั้นบนคอมพิวเตอร์ของคุณเอง แต่ละขั้นคือไฟล์เล็ก ๆ หนึ่งไฟล์ กล่องสีเข้มแสดงไฟล์ กล่องสีเขียวข้างใต้แสดงสิ่งที่ไฟล์พิมพ์ตอนเรารัน บรรทัดที่ขึ้นต้นด้วยเครื่องหมายดอลลาร์คือคำสั่ง และบรรทัดถัดจากนั้นคือคำตอบ คอมพิวเตอร์ของคุณควรพิมพ์เกือบเหมือนกัน ยกเว้นเวลาและตัวเลขยาว ๆ
Where we ran it. A laptop with Windows 11. Inside it, WSL 2 with a new Ubuntu 26.04. The Linux kernel of that WSL is 6.6.114.1-microsoft-standard-WSL2. Nothing else was installed before step 1. On Windows you get to the same starting point with Microsoft's WSL install page. On a computer that already runs Ubuntu, start at step 1. 我们在哪里运行的。一台装有 Windows 11 的笔记本。里面是 WSL 2 和一个全新的 Ubuntu 26.04。这个 WSL 的 Linux 内核是 6.6.114.1-microsoft-standard-WSL2。在第 1 步之前没有安装别的东西。在 Windows 上,按照微软的 WSL 安装页面就能到达同样的起点。如果你的电脑本来就运行 Ubuntu,直接从第 1 步开始。 เรารันที่ไหน แล็ปท็อปที่ใช้ Windows 11 ข้างในมี WSL 2 กับ Ubuntu 26.04 ที่ติดตั้งใหม่ เคอร์เนล Linux ของ WSL นั้นคือ 6.6.114.1-microsoft-standard-WSL2 ก่อนขั้นที่ 1 ไม่ได้ติดตั้งอะไรอื่น บน Windows คุณไปถึงจุดเริ่มต้นเดียวกันได้ด้วย หน้าติดตั้ง WSL ของ Microsoft ถ้าคอมพิวเตอร์ของคุณใช้ Ubuntu อยู่แล้ว ให้เริ่มที่ขั้นที่ 1
mkdir ~/openshell-lab and go into it with cd ~/openshell-lab. Do not keep the
files under /mnt/c. We lost time to this: a file that OpenShell uploads from the Windows
drive arrives full of zeros. The proof is in Part 5.
在 Windows 上,请在 Linux 的主目录里工作,不要在 Windows 盘上。用 mkdir ~/openshell-lab 建一个文件夹,
再用 cd ~/openshell-lab 进去。不要把文件放在 /mnt/c 下面。我们为此浪费过时间:OpenShell 从 Windows 盘上传的文件,
到达时里面全是零。证据在第 5 部分。
บน Windows ให้ทำงานในโฟลเดอร์ home ของ Linux ไม่ใช่บนไดรฟ์ของ Windows สร้างโฟลเดอร์ด้วย mkdir ~/openshell-lab
แล้วเข้าไปด้วย cd ~/openshell-lab อย่าเก็บไฟล์ไว้ใต้ /mnt/c เราเสียเวลากับเรื่องนี้มาแล้ว: ไฟล์ที่ OpenShell
อัปโหลดจากไดรฟ์ของ Windows ไปถึงโดยมีแต่เลขศูนย์ หลักฐานอยู่ในส่วนที่ 5
Get the files. In that folder, paste this block into the terminal. It fetches the twenty-four files of this page from krueng.ai. You can also save them one by one: every dark box has a download link. 取得文件。在那个文件夹里,把下面这一段粘贴到终端。它会从 krueng.ai 取回本页的二十四个文件。你也可以一个一个保存:每个深色方框都有下载链接。 รับไฟล์ ในโฟลเดอร์นั้น วางบล็อกนี้ลงในเทอร์มินัล มันจะดึงไฟล์ยี่สิบสี่ไฟล์ของหน้านี้มาจาก krueng.ai คุณจะบันทึกทีละไฟล์ก็ได้: กล่องสีเข้มทุกกล่องมีลิงก์ดาวน์โหลด
# get_files.sh - fetch this page's files into the folder you are in.
SITE=https://krueng.ai/openshell_assets
for name in check_kernel.py install_docker.sh check_docker.sh install_openshell.sh \
first_try.sh use_vm.sh Dockerfile make_image.sh make_sandbox.sh look_inside.sh \
try_network.sh allow_reading.sh ask_github.py review.sh decide.py predict.sh \
key_server.py notes_api.yaml fetch_notes.py keys.sh make_venv.sh hello_sdk.py \
use_python.sh clean_up.sh; do
curl -fsSO "$SITE/$name"
done
lsWhat it does. A sandbox is built from parts of the Linux kernel, so the first question goes to
the kernel. check_kernel.py asks three things. Which Landlock does it have? That is
the part that fences off files, and OpenShell needs version 3 or newer. What can seccomp do? That
is the part that watches what a program asks for, and OpenShell needs the word
user_notif in its list. And the question most people forget: may a program here have a
seccomp listener? The kernel gives each program one at most.
它做什么。沙盒是用 Linux 内核的几个部分搭起来的,所以第一个问题要问内核。check_kernel.py 问三件事。
它有哪个版本的 Landlock?这是把文件围起来的部分,OpenShell 需要第 3 版或更新。seccomp 能做什么?这是监视程序提出什么请求的部分,
OpenShell 需要它的列表里有 user_notif 这个词。还有多数人会忘记的问题:这里的程序能不能拥有一个 seccomp 监听器?内核给每个程序最多一个。
มันทำอะไร แซนด์บ็อกซ์สร้างจากส่วนต่าง ๆ ของเคอร์เนล Linux คำถามแรกจึงต้องถามเคอร์เนล check_kernel.py ถามสามเรื่อง
มันมี Landlock เวอร์ชันไหน นี่คือส่วนที่ล้อมรั้วไฟล์ และ OpenShell ต้องการเวอร์ชัน 3 ขึ้นไป seccomp ทำอะไรได้บ้าง นี่คือส่วนที่เฝ้าดูว่าโปรแกรมขออะไร
และ OpenShell ต้องการคำว่า user_notif ในรายการ และคำถามที่คนส่วนใหญ่ลืม: โปรแกรมที่นี่มีตัวรับฟังของ seccomp ได้ไหม เคอร์เนลให้แต่ละโปรแกรมได้มากที่สุดหนึ่งตัว
The one hard part. Python has no ready-made way to ask these questions, so the file borrows the
C library with ctypes and makes two system calls by their numbers. A system call is a
request from a program to the kernel. The two small classes describe a seccomp filter in the layout
the kernel expects. The request for a listener is made in a child process that ends at once, so
nothing on your computer stays changed.
唯一难的地方。Python 没有现成的办法问这些问题,所以这个文件用 ctypes 借用 C 库,按编号发出两个系统调用。
系统调用就是程序向内核提出的请求。那两个小类按内核期望的格式描述一个 seccomp 过滤器。要监听器的请求是在一个立刻结束的子进程里发出的,所以你的电脑上不会留下任何改动。
ส่วนเดียวที่ยาก Python ไม่มีวิธีสำเร็จรูปสำหรับถามคำถามเหล่านี้ ไฟล์จึงยืมไลบรารี C ด้วย ctypes แล้วเรียก system call สองตัวด้วยหมายเลข
system call คือคำขอจากโปรแกรมถึงเคอร์เนล คลาสเล็ก ๆ สองคลาสบรรยายตัวกรองของ seccomp ในรูปแบบที่เคอร์เนลต้องการ คำขอตัวรับฟังถูกส่งในโปรเซสลูกที่จบทันที
จึงไม่มีอะไรบนคอมพิวเตอร์ของคุณเปลี่ยนค้างไว้
"""check_kernel.py - a first look: could this computer run an OpenShell sandbox?
python3 check_kernel.py
OpenShell builds its sandbox from two parts of the Linux kernel. Landlock
fences off files. seccomp watches what a program asks the kernel to do, and
OpenShell asks seccomp for a "listener", so that it can answer those requests.
This file asks the kernel three things: which Landlock it has, what seccomp
can do, and whether a program here may have a listener. It changes nothing.
The one request that could change something is made in a child process, and
the child ends at once.
It is only a first look. OpenShell runs its own, stricter tests every time it
starts a sandbox, and refuses to start one that fails them.
"""
import ctypes
import errno
import os
import platform
LANDLOCK_NEEDED = 3 # OpenShell asks for Landlock version 3 or newer
LANDLOCK_CREATE_RULESET = 444 # the kernel's number for this request
ONLY_THE_VERSION = 1 # "make nothing, just say your version"
SECCOMP = {"x86_64": 317, "aarch64": 277} # the number of seccomp() on each chip
SET_A_FILTER = 1
WITH_A_LISTENER = 1 << 3
NO_NEW_PRIVILEGES = 38 # a program must promise this before it sets a filter
ALLOW_EVERYTHING = (0x06, 0, 0, 0x7FFF0000) # a filter of one line
SECCOMP_ACTIONS = "/proc/sys/kernel/seccomp/actions_avail"
class FilterLine(ctypes.Structure):
"""One line of a seccomp filter, laid out the way the kernel reads it."""
_fields_ = [("code", ctypes.c_ushort), ("jump_true", ctypes.c_ubyte),
("jump_false", ctypes.c_ubyte), ("value", ctypes.c_uint)]
class Filter(ctypes.Structure):
"""A whole filter: how many lines it has, and where they are."""
_fields_ = [("length", ctypes.c_ushort), ("lines", ctypes.POINTER(FilterLine))]
def landlock_version():
"""The Landlock version this kernel offers, or 0 if it has none."""
c_library = ctypes.CDLL(None, use_errno=True)
version = c_library.syscall(LANDLOCK_CREATE_RULESET, None, 0, ONLY_THE_VERSION)
return max(version, 0) # the kernel answers -1 when it has no Landlock
def seccomp_actions():
"""What seccomp can do on this kernel, as a list of words."""
if not os.path.exists(SECCOMP_ACTIONS):
return []
with open(SECCOMP_ACTIONS) as file:
return file.read().split()
def filters_already():
"""How many seccomp filters this program was started with. Usually none."""
with open("/proc/self/status") as file:
for line in file:
if line.startswith("Seccomp_filters:"):
return int(line.split()[1])
return 0
def ask_for_listener():
"""Ask the kernel for a listener. Returns 0, or the kernel's error number."""
c_library = ctypes.CDLL(None, use_errno=True)
c_library.prctl(NO_NEW_PRIVILEGES, 1, 0, 0, 0)
lines = (FilterLine * 1)(FilterLine(*ALLOW_EVERYTHING))
answer = c_library.syscall(SECCOMP[platform.machine()], SET_A_FILTER,
WITH_A_LISTENER, ctypes.byref(Filter(1, lines)))
return 0 if answer >= 0 else ctypes.get_errno()
def listener_error():
"""What a child process was told when it asked for a listener: 0 means yes."""
read_end, write_end = os.pipe()
child = os.fork()
if child == 0: # this is the child: ask, report, end
os.write(write_end, str(ask_for_listener()).encode())
os._exit(0)
os.waitpid(child, 0)
return int(os.read(read_end, 16))
def main():
print("system :", platform.system(), platform.release())
print("chip :", platform.machine())
if platform.system() != "Linux":
print("This check needs a Linux kernel. On Windows, run it inside WSL 2.")
print("On a Mac, the sandbox uses the Linux that Docker Desktop runs.")
return
version = landlock_version()
actions = seccomp_actions()
error = listener_error()
print("Landlock:", f"version {version}" if version else "none")
print("seccomp :", " ".join(actions) if actions else "none")
print("filters :", filters_already(), "already on this program before it asked")
if error == 0:
print("listener: yes")
else:
print("listener: no, the kernel answered", errno.errorcode[error],
f"({os.strerror(error)})")
ready = version >= LANDLOCK_NEEDED and "user_notif" in actions and error == 0
print("a container sandbox looks possible:", "yes" if ready else "no")
if error == errno.EBUSY:
print("Something else already holds the one listener a program may have.")
print("OpenShell's VM runtime still works: each sandbox gets its own kernel.")
if __name__ == "__main__":
main()system : Linux 6.6.114.1-microsoft-standard-WSL2
chip : x86_64
Landlock: version 3
seccomp : kill_process kill_thread trap errno user_notif trace log allow
filters : 1 already on this program before it asked
listener: no, the kernel answered EBUSY (Device or resource busy)
a container sandbox looks possible: no
Something else already holds the one listener a program may have.
OpenShell's VM runtime still works: each sandbox gets its own kernel.Read the output. Landlock version 3: enough. seccomp lists
user_notif: good. Then two lines that belong together. filters : 1 says that
a seccomp filter was already on our program before it asked for anything. And
listener: no, with the error EBUSY, which means “busy”: something else on our
laptop already holds the one listener, and it was there before our program started. Step 4 shows
what OpenShell does about that, and Part 5 says what the something is.
读输出。Landlock 版本 3:够了。seccomp 列出了 user_notif:很好。接下来的两行要连起来看。
filters : 1 说明在我们的程序提出任何请求之前,它身上就已经有一个 seccomp 过滤器了。还有 listener: no,错误是 EBUSY,意思是“忙”:
我们笔记本上有别的东西已经占了那唯一的监听器,而且在我们的程序启动之前它就在那里了。第 4 步展示 OpenShell 对此怎么做,第 5 部分说明那个“别的东西”是什么。
อ่านผลลัพธ์ Landlock เวอร์ชัน 3: พอแล้ว seccomp มี user_notif ในรายการ: ดี จากนั้นมีสองบรรทัดที่ต้องอ่านคู่กัน
filters : 1 บอกว่ามีตัวกรอง seccomp อยู่บนโปรแกรมของเราแล้วก่อนที่มันจะขออะไร และ listener: no พร้อมข้อผิดพลาด EBUSY ซึ่งแปลว่า “ไม่ว่าง”:
มีอย่างอื่นบนแล็ปท็อปของเราถือตัวรับฟังตัวเดียวนั้นไว้แล้ว และมันอยู่ตรงนั้นก่อนที่โปรแกรมของเราจะเริ่ม ขั้นที่ 4 แสดงว่า OpenShell ทำอย่างไรกับเรื่องนี้ และส่วนที่ 5 บอกว่าอย่างอื่นนั้นคืออะไร
The other answer. We ran the same file on a second computer: a small NVIDIA machine that runs
Ubuntu with no Windows under it. There the kernel said yes. If your computer prints
listener: yes, step 5 is not for you.
另一种回答。我们在第二台电脑上运行了同一个文件:一台小型的 NVIDIA 机器,它运行 Ubuntu,下面没有 Windows。在那里,内核回答“可以”。
如果你的电脑打印 listener: yes,第 5 步你用不着。
คำตอบอีกแบบหนึ่ง เรารันไฟล์เดียวกันบนคอมพิวเตอร์เครื่องที่สอง: เครื่อง NVIDIA ขนาดเล็กที่ใช้ Ubuntu โดยไม่มี Windows อยู่ข้างใต้ ที่นั่นเคอร์เนลตอบว่าได้
ถ้าคอมพิวเตอร์ของคุณพิมพ์ listener: yes ขั้นที่ 5 ไม่ใช่สำหรับคุณ
system : Linux 6.17.0-1018-nvidia
chip : aarch64
Landlock: version 7
seccomp : kill_process kill_thread trap errno user_notif trace log allow
filters : 0 already on this program before it asked
listener: yes
a container sandbox looks possible: yesWhat it does. OpenShell uses Docker to fetch and build images. Ubuntu has Docker in its own
package list, so three commands are enough. docker.io is Docker itself, and
docker-buildx is the part that builds images. The last command puts your user in the
group called docker, so that you do not need sudo for every Docker command.
apt told us it would download 91.8 MB and use
357 MB of disk.
它做什么。OpenShell 用 Docker 来获取和构建镜像。Ubuntu 自己的软件包列表里就有 Docker,所以三条命令就够了。
docker.io 是 Docker 本身,docker-buildx 是构建镜像的部分。最后一条命令把你的用户加入名为 docker 的组,
这样就不用每条 Docker 命令都加 sudo。apt 告诉我们它要下载 91.8 MB,占用 357 MB 磁盘。
มันทำอะไร OpenShell ใช้ Docker เพื่อดึงและสร้างอิมเมจ Ubuntu มี Docker อยู่ในรายการแพ็กเกจของตัวเอง สามคำสั่งจึงพอ
docker.io คือตัว Docker และ docker-buildx คือส่วนที่สร้างอิมเมจ คำสั่งสุดท้ายใส่ผู้ใช้ของคุณเข้ากลุ่มชื่อ docker
เพื่อไม่ต้องพิมพ์ sudo ทุกครั้งที่ใช้ Docker apt บอกเราว่าจะดาวน์โหลด 91.8 MB และใช้ดิสก์ 357 MB
# install_docker.sh - Docker Engine, from Ubuntu's own packages.
# docker-buildx is the part of Docker that builds images.
sudo apt-get update
sudo apt-get install -y docker.io docker-buildx
# Let your user talk to Docker without sudo.
# This takes effect the next time you log in.
sudo usermod -aG docker "$USER"Run it with bash install_docker.sh, or type the three commands yourself. What it
printed for us is long and dull: the whole log is here.
Then log in again, because a new group only counts from your next login. On Windows: close the
Ubuntu window, type wsl --shutdown in PowerShell, and open Ubuntu again. On a Linux
computer: log out and log in. Then check:
运行它:bash install_docker.sh,或者自己输入这三条命令。它给我们打印的内容又长又无聊:
完整的日志在这里。然后重新登录,因为新的组要到你下次登录才算数。
在 Windows 上:关掉 Ubuntu 窗口,在 PowerShell 里输入 wsl --shutdown,再打开 Ubuntu。在 Linux 电脑上:退出登录再登录。然后检查:
รันมันด้วย bash install_docker.sh หรือพิมพ์สามคำสั่งเอง สิ่งที่มันพิมพ์ให้เรายาวและน่าเบื่อ:
ล็อกทั้งหมดอยู่ที่นี่ จากนั้นล็อกอินใหม่ เพราะกลุ่มใหม่จะมีผลตั้งแต่การล็อกอินครั้งถัดไป
บน Windows: ปิดหน้าต่าง Ubuntu พิมพ์ wsl --shutdown ใน PowerShell แล้วเปิด Ubuntu อีกครั้ง บนคอมพิวเตอร์ Linux: ออกจากระบบแล้วเข้าใหม่ จากนั้นตรวจ:
# check_docker.sh - does Docker answer you, without sudo?
docker --version
docker ps$ docker --version
Docker version 29.1.3, build 29.1.3-0ubuntu4.1
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESRead the output. Docker 29.1.3; OpenShell asks for 28.0 or newer.
docker ps prints a table with no rows, and no error about permission: your user may talk
to Docker. One honest warning for a page about safety: a user in the docker group can do
almost anything on that computer through Docker. The sandbox protects your computer from the program
inside it. It does not protect the computer from you.
读输出。Docker 29.1.3;OpenShell 要求 28.0 或更新。docker ps 打印出一张没有行的表格,
也没有权限错误:你的用户可以和 Docker 对话。在一个讲安全的页面上要老实提醒一句:属于 docker 组的用户几乎可以通过 Docker 在那台电脑上做任何事。
沙盒保护你的电脑不受里面那个程序的伤害。它不保护电脑不受你的伤害。
อ่านผลลัพธ์ Docker 29.1.3 ส่วน OpenShell ต้องการ 28.0 ขึ้นไป docker ps พิมพ์ตารางที่ไม่มีแถว
และไม่มีข้อผิดพลาดเรื่องสิทธิ์: ผู้ใช้ของคุณคุยกับ Docker ได้ คำเตือนตรง ๆ หนึ่งข้อสำหรับหน้าที่พูดเรื่องความปลอดภัย: ผู้ใช้ในกลุ่ม docker
ทำได้เกือบทุกอย่างบนคอมพิวเตอร์เครื่องนั้นผ่าน Docker แซนด์บ็อกซ์ปกป้องคอมพิวเตอร์ของคุณจากโปรแกรมที่อยู่ข้างใน มันไม่ได้ปกป้องคอมพิวเตอร์จากตัวคุณ
What it does. OpenShell's README gives one line that downloads an installer and runs it at
once. We use two lines: the first saves the installer as install.sh, the second runs it.
Between the two you can open the file and read it, which is the right habit on a page about not
trusting programs blindly. The installer finds the newest release, downloads one package of
74.5 MB, checks its fingerprint, installs it, and starts the
gateway as a service of your user. The last three commands ask the gateway how it is.
它做什么。OpenShell 的 README 给出一行命令:下载安装程序并立刻运行。我们用两行:第一行把安装程序存为 install.sh,
第二行运行它。在两行之间你可以打开这个文件读一读,在一个讲“不要盲目信任程序”的页面上,这是应有的习惯。安装程序找到最新的发布版,下载一个
74.5 MB 的软件包,核对它的指纹,安装它,并把网关作为你这个用户的服务启动。最后三条命令问网关它怎么样。
มันทำอะไร README ของ OpenShell ให้คำสั่งหนึ่งบรรทัดที่ดาวน์โหลดตัวติดตั้งแล้วรันทันที เราใช้สองบรรทัด: บรรทัดแรกบันทึกตัวติดตั้งเป็น install.sh
บรรทัดที่สองรันมัน ระหว่างสองบรรทัดนี้คุณเปิดไฟล์อ่านได้ ซึ่งเป็นนิสัยที่ถูกต้องในหน้าที่สอนว่าอย่าไว้ใจโปรแกรมแบบหลับหูหลับตา ตัวติดตั้งหารุ่นล่าสุด ดาวน์โหลดแพ็กเกจหนึ่งตัวขนาด
74.5 MB ตรวจลายนิ้วมือของมัน ติดตั้ง แล้วเริ่มเกตเวย์เป็นบริการของผู้ใช้ของคุณ สามคำสั่งสุดท้ายถามเกตเวย์ว่าเป็นอย่างไร
# install_openshell.sh - download OpenShell's installer, then run it.
# Two lines, so that you can read install.sh before you run it.
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh -o install.sh
sh install.sh
# Is the gateway up, which version did you get, and what does its own check say?
openshell status
openshell --version
openshell doctor check$ curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh -o install.sh
$ sh install.sh
openshell: resolving latest version...
openshell: downloading v0.1.2 release checksums...
openshell: selected openshell_0.1.2-1_amd64.deb
openshell: downloading openshell_0.1.2-1_amd64.deb...
openshell: verifying checksum...
openshell: installing openshell_0.1.2-1_amd64.deb...
Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
The following NEW packages will be installed:
openshell
0 upgraded, 1 newly installed, 0 to remove and 103 not upgraded.
Need to get 0 B/74.5 MB of archives.
After this operation, 0 B of additional disk space will be used.
Get:1 /tmp/tmp.c3IPT2HAcx/openshell_0.1.2-1_amd64.deb openshell amd64 0.1.2-1 [74.5 MB]
Selecting previously unselected package openshell.
(Reading database ... 36566 files and directories currently installed.)
Preparing to unpack .../openshell_0.1.2-1_amd64.deb ...
Unpacking openshell (0.1.2-1) ...
Setting up openshell (0.1.2-1) ...
openshell: installed openshell package from v0.1.2
openshell: restarting openshell-gateway user service as student...
Created symlink '/home/student/.config/systemd/user/default.target.wants/openshell-gateway.service' → '/usr/lib/systemd/user/openshell-gateway.service'.
openshell: registering local gateway as student...
✓ Gateway 'openshell' added and set as active
Endpoint: https://127.0.0.1:17670
Type: local
✓ TLS certificates present
openshell: waiting for local gateway listener to become reachable...
openshell: local gateway listener is reachable
openshell: waiting for openshell status to report connected...
openshell: openshell status reports connected
$ openshell status
Server Status
Gateway: openshell
Server: https://127.0.0.1:17670
Status: Connected
Authentication: Authenticated (mTLS transport)
Version: 0.1.2
$ openshell --version
openshell 0.1.2
$ openshell doctor check
Checking system prerequisites...
Docker ............. ok (version 29.1.3)
DOCKER_HOST ........ (not set, using default socket)
All checks passed.Read the output. Scroll to the end of the green box. Status: Connected means the
openshell command reached the gateway at https://127.0.0.1:17670, an address
that only your own computer can use. The version is 0.1.2.
The gateway's own check says All checks passed. Remember that line. It checked Docker.
It did not ask the question of step 1.
读输出。滚动到绿色方框的末尾。Status: Connected 表示 openshell 命令连上了位于
https://127.0.0.1:17670 的网关,这是一个只有你自己的电脑能用的地址。版本是 0.1.2。
网关自己的检查说 All checks passed。记住这一行。它检查了 Docker,却没有问第 1 步的那个问题。
อ่านผลลัพธ์ เลื่อนไปท้ายกล่องสีเขียว Status: Connected หมายความว่าคำสั่ง openshell ติดต่อเกตเวย์ที่
https://127.0.0.1:17670 ได้ ซึ่งเป็นที่อยู่ที่ใช้ได้เฉพาะคอมพิวเตอร์ของคุณเอง เวอร์ชันคือ 0.1.2
การตรวจของเกตเวย์เองบอกว่า All checks passed จำบรรทัดนี้ไว้ มันตรวจ Docker แต่ไม่ได้ถามคำถามของขั้นที่ 1
What it does. One command makes a sandbox. We gave no runtime, so OpenShell chose Docker, and
we gave no image, so it fetched its own bare Ubuntu. --detach means: start it and give me
my terminal back.
它做什么。一条命令就能创建沙盒。我们没有指定运行时,所以 OpenShell 选了 Docker;我们也没有指定镜像,所以它取来自己那个最精简的 Ubuntu。
--detach 的意思是:启动它,然后把终端还给我。
มันทำอะไร คำสั่งเดียวสร้างแซนด์บ็อกซ์ได้ เราไม่ได้ระบุรันไทม์ OpenShell จึงเลือก Docker และเราไม่ได้ระบุอิมเมจ มันจึงดึง Ubuntu เปล่า ๆ ของตัวเองมา
--detach หมายความว่า: เริ่มมัน แล้วคืนเทอร์มินัลให้ฉัน
# first_try.sh - try a sandbox with the runtime OpenShell picks by itself.
openshell sandbox create --name first --detach
openshell sandbox list
openshell sandbox delete first$ openshell sandbox create --name first --detach
Created sandbox: first
[0.0s] Requesting compute...
[0.0s] Sandbox allocated
[0.0s] Pulling image nvcr.io/nvidia/base/ubuntu:24.04
[0.0s] Pulling image nvcr.io/nvidia/base/ubuntu:24.04
[2.6s] Pulling image Pulling from nvidia/base/ubuntu 24.04
[3.3s] Pulling image Pulling fs layer 2578028fa103
[3.3s] Pulling image Pulling fs layer edd1ed89f0d4
[3.8s] Pulling image Downloading edd1ed89f0d4 (1 MB/28 MB)
[3.9s] Pulling image Downloading edd1ed89f0d4 (2 MB/28 MB)
[4.0s] Pulling image Downloading edd1ed89f0d4 (4 MB/28 MB)
[4.1s] Pulling image Downloading edd1ed89f0d4 (5 MB/28 MB)
[4.2s] Pulling image Downloading edd1ed89f0d4 (5 MB/28 MB)
[4.3s] Pulling image Downloading edd1ed89f0d4 (6 MB/28 MB)
[4.4s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.4s] Pulling image Downloading edd1ed89f0d4 (7 MB/28 MB)
[4.5s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.5s] Pulling image Downloading edd1ed89f0d4 (7 MB/28 MB)
[4.6s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.6s] Pulling image Downloading edd1ed89f0d4 (8 MB/28 MB)
[4.7s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.7s] Pulling image Downloading edd1ed89f0d4 (8 MB/28 MB)
[4.8s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.8s] Pulling image Downloading edd1ed89f0d4 (9 MB/28 MB)
[4.9s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[4.9s] Pulling image Downloading edd1ed89f0d4 (9 MB/28 MB)
[5.0s] Pulling image Downloading 2578028fa103 (1 MB/4 MB)
[5.0s] Pulling image Downloading edd1ed89f0d4 (10 MB/28 MB)
[5.1s] Pulling image Downloading 2578028fa103 (2 MB/4 MB)
[5.1s] Pulling image Downloading edd1ed89f0d4 (11 MB/28 MB)
[5.2s] Pulling image Downloading 2578028fa103 (2 MB/4 MB)
[5.2s] Pulling image Downloading edd1ed89f0d4 (11 MB/28 MB)
[5.3s] Pulling image Downloading 2578028fa103 (2 MB/4 MB)
[5.3s] Pulling image Downloading edd1ed89f0d4 (12 MB/28 MB)
[5.4s] Pulling image Downloading 2578028fa103 (2 MB/4 MB)
[5.4s] Pulling image Downloading edd1ed89f0d4 (12 MB/28 MB)
[5.5s] Pulling image Downloading 2578028fa103 (2 MB/4 MB)
[5.5s] Pulling image Downloading edd1ed89f0d4 (12 MB/28 MB)
[5.6s] Pulling image Downloading 2578028fa103 (3 MB/4 MB)
[5.6s] Pulling image Downloading edd1ed89f0d4 (13 MB/28 MB)
[5.7s] Pulling image Downloading 2578028fa103 (3 MB/4 MB)
[5.7s] Pulling image Downloading edd1ed89f0d4 (13 MB/28 MB)
[5.8s] Pulling image Downloading 2578028fa103 (3 MB/4 MB)
[5.8s] Pulling image Downloading edd1ed89f0d4 (14 MB/28 MB)
[5.9s] Pulling image Downloading edd1ed89f0d4 (14 MB/28 MB)
[5.9s] Pulling image Downloading 2578028fa103 (3 MB/4 MB)
[6.0s] Pulling image Downloading 2578028fa103 (3 MB/4 MB)
[6.0s] Pulling image Downloading edd1ed89f0d4 (14 MB/28 MB)
[6.1s] Pulling image Downloading 2578028fa103 (4 MB/4 MB)
[6.1s] Pulling image Downloading edd1ed89f0d4 (15 MB/28 MB)
[6.2s] Pulling image Downloading 2578028fa103 (4 MB/4 MB)
[6.2s] Pulling image Downloading edd1ed89f0d4 (15 MB/28 MB)
[6.3s] Pulling image Downloading 2578028fa103 (4 MB/4 MB)
[6.3s] Pulling image Downloading edd1ed89f0d4 (16 MB/28 MB)
[6.4s] Pulling image Download complete 2578028fa103
[6.4s] Pulling image Downloading edd1ed89f0d4 (16 MB/28 MB)
[6.5s] Pulling image Downloading edd1ed89f0d4 (17 MB/28 MB)
[6.6s] Pulling image Downloading edd1ed89f0d4 (18 MB/28 MB)
[6.7s] Pulling image Downloading edd1ed89f0d4 (18 MB/28 MB)
[6.8s] Pulling image Downloading edd1ed89f0d4 (19 MB/28 MB)
[6.9s] Pulling image Downloading edd1ed89f0d4 (20 MB/28 MB)
[7.0s] Pulling image Downloading edd1ed89f0d4 (20 MB/28 MB)
[7.1s] Pulling image Downloading edd1ed89f0d4 (21 MB/28 MB)
[7.2s] Pulling image Downloading edd1ed89f0d4 (22 MB/28 MB)
[7.3s] Pulling image Downloading edd1ed89f0d4 (22 MB/28 MB)
[7.4s] Pulling image Downloading edd1ed89f0d4 (23 MB/28 MB)
[7.5s] Pulling image Downloading edd1ed89f0d4 (24 MB/28 MB)
[7.6s] Pulling image Downloading edd1ed89f0d4 (24 MB/28 MB)
[7.7s] Pulling image Downloading edd1ed89f0d4 (25 MB/28 MB)
[7.8s] Pulling image Downloading edd1ed89f0d4 (25 MB/28 MB)
[7.9s] Pulling image Downloading edd1ed89f0d4 (26 MB/28 MB)
[8.0s] Pulling image Downloading edd1ed89f0d4 (26 MB/28 MB)
[8.1s] Pulling image Downloading edd1ed89f0d4 (27 MB/28 MB)
[8.2s] Pulling image Downloading edd1ed89f0d4 (28 MB/28 MB)
[8.3s] Pulling image Downloading edd1ed89f0d4 (28 MB/28 MB)
[8.4s] Pulling image Download complete edd1ed89f0d4
[8.4s] Pulling image Extracting edd1ed89f0d4 (1 B)
[8.5s] Pulling image Extracting edd1ed89f0d4 (1 B)
[8.6s] Pulling image Extracting edd1ed89f0d4 (1 B)
[8.7s] Pulling image Extracting edd1ed89f0d4 (1 B)
[8.8s] Pulling image Extracting edd1ed89f0d4 (1 B)
[8.9s] Pulling image Extracting edd1ed89f0d4 (1 B)
[9.0s] Pulling image Extracting edd1ed89f0d4 (1 B)
[9.1s] Pulling image Extracting 2578028fa103 (1 B)
[9.1s] Pulling image Pull complete edd1ed89f0d4
[9.2s] Pulling image Pull complete 2578028fa103
[9.2s] Pulling image Digest: sha256:43c1682f10cdf6561e22f6da1765d491b6f0e5808f94d5fde29d7bdbe75142d5
[9.2s] Pulling image Status: Downloaded newer image for nvcr.io/nvidia/base/ubuntu:24.04
[9.2s] Image pulled
[9.4s] Starting sandbox Container created
Error: × sandbox entered error phase while provisioning:
│ ControlSupervisorStartFailed: Docker sandbox exited before supervisor
│ became ready; sandbox log tail: Error: × seccomp notification probe
│ ╰─▶ notification launcher disappeared
$ openshell sandbox list
NAME CREATED PHASE
first 2026-10-09 14:08:04 Error
$ openshell sandbox delete first
✓ Sandbox first deletion accepted; cleanup is pendingRead the output. The image arrives, the container is made, and then:
seccomp notification probe … notification launcher disappeared. Before
OpenShell lets any program into a sandbox, the sandbox tests its own walls. One test asks the kernel
for a listener, which is the request that failed in step 1. The test fails, so the sandbox refuses to
start, and its phase is Error. This is called failing closed: when the guard
cannot do its job, nothing runs. A tool that started the sandbox anyway, without the guard, would be
far more dangerous than one that says no.
读输出。镜像到了,容器也建好了,然后:seccomp notification probe … notification launcher disappeared。
在 OpenShell 让任何程序进入沙盒之前,沙盒会先测试自己的墙。其中一项测试是向内核要一个监听器,也就是第 1 步里失败的那个请求。测试失败了,
所以沙盒拒绝启动,它的阶段是 Error。这叫做失败即关闭:当守卫没法工作时,什么都不运行。一个不带守卫也照样启动沙盒的工具,
会比一个说“不”的工具危险得多。
อ่านผลลัพธ์ อิมเมจมาถึง คอนเทนเนอร์ถูกสร้าง แล้วก็: seccomp notification probe … notification launcher disappeared
ก่อนที่ OpenShell จะปล่อยให้โปรแกรมใดเข้าแซนด์บ็อกซ์ แซนด์บ็อกซ์จะทดสอบกำแพงของตัวเองก่อน การทดสอบหนึ่งคือการขอตัวรับฟังจากเคอร์เนล ซึ่งเป็นคำขอที่ล้มเหลวในขั้นที่ 1
การทดสอบล้มเหลว แซนด์บ็อกซ์จึงปฏิเสธที่จะเริ่ม และสถานะของมันคือ Error เรียกว่าล้มเหลวแบบปิด: เมื่อยามทำงานไม่ได้ ก็ไม่มีอะไรได้รัน
เครื่องมือที่ยอมเริ่มแซนด์บ็อกซ์ทั้งที่ไม่มียาม จะอันตรายกว่าเครื่องมือที่ตอบว่าไม่มาก
If yours says Ready, your computer has a free listener and the Docker runtime works
for you. Skip step 5. We saw Ready here only in a later test, with WSL in another
mode (Part 5). We did not run the steps after this one with the Docker runtime.
如果你的显示 Ready,说明你的电脑有空闲的监听器,Docker 运行时对你可用。跳过第 5 步。我们自己只在后来的一次测试里、
WSL 处于另一种模式时,在这里看到过 Ready(第 5 部分)。这一步之后的步骤,我们没有用 Docker 运行时跑过。
ถ้าของคุณขึ้นว่า Ready คอมพิวเตอร์ของคุณมีตัวรับฟังว่าง และรันไทม์ Docker ใช้ได้สำหรับคุณ ข้ามขั้นที่ 5 ไป
เราเองเห็น Ready ตรงนี้เฉพาะในการทดสอบภายหลัง ตอนที่ WSL อยู่ในอีกโหมดหนึ่ง (ส่วนที่ 5) เราไม่ได้รันขั้นหลังจากนี้ด้วยรันไทม์ Docker
What it does. OpenShell has a second runtime that puts each sandbox in a small virtual
machine. A virtual machine has a kernel of its own, and inside that kernel the listener is free.
Two things are needed. Your user must be allowed to start virtual machines, which on Linux means the
group kvm. And the gateway must be told which runtime to use: it reads the file
gateway.env when it starts, and the line in it is the one from OpenShell's manual.
它做什么。OpenShell 还有第二种运行时,它把每个沙盒放进一台小小的虚拟机。虚拟机有自己的内核,在那个内核里监听器是空闲的。
需要两件事。你的用户必须被允许启动虚拟机,在 Linux 上这意味着加入 kvm 组。还要告诉网关使用哪种运行时:它启动时会读取
gateway.env 文件,里面那一行出自 OpenShell 的手册。
มันทำอะไร OpenShell มีรันไทม์ที่สองซึ่งใส่แซนด์บ็อกซ์แต่ละอันไว้ในเครื่องเสมือนขนาดเล็ก เครื่องเสมือนมีเคอร์เนลของตัวเอง และในเคอร์เนลนั้นตัวรับฟังยังว่าง
ต้องทำสองอย่าง ผู้ใช้ของคุณต้องได้รับอนุญาตให้เริ่มเครื่องเสมือน ซึ่งบน Linux หมายถึงกลุ่ม kvm และต้องบอกเกตเวย์ว่าจะใช้รันไทม์ไหน:
มันอ่านไฟล์ gateway.env ตอนเริ่มทำงาน และบรรทัดในไฟล์นั้นมาจากคู่มือของ OpenShell
# use_vm.sh - give every sandbox its own small virtual machine.
# Your user needs the right to start virtual machines.
sudo usermod -aG kvm "$USER"
# Tell the gateway which runtime to use. It reads this file when it starts.
mkdir -p ~/.config/openshell
echo "OPENSHELL_COMPUTE_DRIVER=vm" > ~/.config/openshell/gateway.env
cat ~/.config/openshell/gateway.env$ sudo usermod -aG kvm "$USER"
$ mkdir -p ~/.config/openshell
$ echo "OPENSHELL_COMPUTE_DRIVER=vm" > ~/.config/openshell/gateway.env
$ cat ~/.config/openshell/gateway.env
OPENSHELL_COMPUTE_DRIVER=vmThen log in again, as in step 2, for the same reason: a new group. The gateway starts again
with your login and reads the new file. A virtual machine inside WSL needs Windows 11 and a processor
that can run a virtual machine inside another one. Ours could. If ls /dev/kvm finds
nothing on your computer, this runtime is not for you.
然后重新登录,和第 2 步一样,原因也一样:新的组。网关会随着你的登录重新启动,并读取新文件。在 WSL 里运行虚拟机需要 Windows 11,
以及一个能在虚拟机里再运行虚拟机的处理器。我们的可以。如果 ls /dev/kvm 在你的电脑上什么都找不到,这种运行时就不适合你。
จากนั้นล็อกอินใหม่ เหมือนขั้นที่ 2 ด้วยเหตุผลเดียวกัน: กลุ่มใหม่ เกตเวย์จะเริ่มใหม่พร้อมการล็อกอินของคุณและอ่านไฟล์ใหม่ เครื่องเสมือนภายใน WSL
ต้องใช้ Windows 11 และโปรเซสเซอร์ที่รันเครื่องเสมือนซ้อนในเครื่องเสมือนได้ ของเราทำได้ ถ้า ls /dev/kvm ไม่พบอะไรบนคอมพิวเตอร์ของคุณ รันไทม์นี้ไม่เหมาะกับคุณ
What it does. A sandbox starts as a copy of an image. The image OpenShell uses when you
name none has no curl and no Python (Part 5 shows it), and we need both to test with. A
Dockerfile is the recipe for an image. Ours has two instructions: start FROM
the same Ubuntu that OpenShell uses, then RUN one command that installs the tools.
它做什么。沙盒一开始是某个镜像的副本。当你不指定镜像时,OpenShell 用的那个镜像里既没有 curl 也没有 Python(第 5 部分有证据),
而我们做测试两样都需要。Dockerfile 是镜像的配方。我们的只有两条指令:FROM 表示从 OpenShell 用的同一个 Ubuntu 开始,
RUN 执行一条安装工具的命令。
มันทำอะไร แซนด์บ็อกซ์เริ่มจากสำเนาของอิมเมจ อิมเมจที่ OpenShell ใช้เมื่อคุณไม่ระบุนั้นไม่มี curl และไม่มี Python (ส่วนที่ 5 แสดงหลักฐาน)
แต่เราต้องใช้ทั้งสองอย่างในการทดสอบ Dockerfile คือสูตรของอิมเมจ ของเรามีสองคำสั่ง: เริ่ม FROM Ubuntu ตัวเดียวกับที่ OpenShell ใช้
แล้ว RUN คำสั่งหนึ่งที่ติดตั้งเครื่องมือ
# Dockerfile - the sandbox's usual Ubuntu, plus two tools to test with.
FROM nvcr.io/nvidia/base/ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl python3 \
&& rm -rf /var/lib/apt/lists/*# make_image.sh - build the image that the Dockerfile in this folder describes.
# Its name will be lab-tools, version 1. Leave out --quiet to watch the build.
docker build --quiet -t lab-tools:1 .$ docker build --quiet -t lab-tools:1 .
sha256:a007391f9a47348711f1c38c51ed068b396a77220d2a25cf0a45af7a0b9b11e5The long line that starts with sha256 is the image's fingerprint: the build worked. Now
the sandbox. --from lab-tools:1 names our image.
以 sha256 开头的那一长行是镜像的指纹:构建成功了。现在创建沙盒。--from lab-tools:1 指定了我们的镜像。
บรรทัดยาวที่ขึ้นต้นด้วย sha256 คือลายนิ้วมือของอิมเมจ: การสร้างสำเร็จ ตอนนี้ถึงแซนด์บ็อกซ์ --from lab-tools:1 ระบุอิมเมจของเรา
# make_sandbox.sh - start a sandbox named demo from our image, and leave it running.
openshell sandbox create --name demo --from lab-tools:1 --detach
openshell sandbox list$ openshell sandbox create --name demo --from lab-tools:1 --detach
Created sandbox: demo
[0.0s] Requesting compute...
[0.0s] Sandbox allocated
[0.0s] Resolving image
[0.0s] Pulling image nvcr.io/nvidia/base/ubuntu:24.04
[0.0s] Preparing image cache
[0.0s] Waiting for image cache lock
[0.0s] Exporting local image rootfs
[2.4s] Preparing rootfs
[2.8s] Formatting root disk
[3.2s] Image pulled (512 MB)
[3.2s] Preparing image cache
[3.2s] Exporting local image rootfs
[5.7s] Formatting root disk
[6.9s] Preparing rootfs
[9.9s] Preparing overlay disk
[10.3s] Waiting for VM supervisor
$ openshell sandbox list
NAME CREATED PHASE
demo 2026-10-09 14:08:53 ReadyRead the output. Waiting for VM supervisor tells you that the new runtime is in use,
and the last line gives the phase: Ready. The numbers in square brackets are seconds
since the command began, so you can read from the box how long each part took.
读输出。Waiting for VM supervisor 告诉你新的运行时正在使用,最后一行给出了阶段:Ready。
方括号里的数字是从命令开始算起的秒数,所以你可以从方框里读出每个部分花了多久。
อ่านผลลัพธ์ Waiting for VM supervisor บอกคุณว่ารันไทม์ใหม่ถูกใช้อยู่ และบรรทัดสุดท้ายบอกสถานะ: Ready
ตัวเลขในวงเล็บเหลี่ยมคือจำนวนวินาทีนับจากเริ่มคำสั่ง คุณจึงอ่านจากกล่องได้ว่าแต่ละส่วนใช้เวลานานเท่าไร
What it does. openshell sandbox exec -n demo -- <command> runs one command
inside the sandbox named demo and brings back what it printed. Everything after the two
dashes is the command. We ask who we are, where we are, which kernel is under us, and which special
powers we hold. Then we try to write in four places.
它做什么。openshell sandbox exec -n demo -- <command> 在名为 demo 的沙盒里运行一条命令,并把它打印的内容带回来。
两个短横线后面的全部内容就是那条命令。我们问:我们是谁、在哪里、脚下是哪个内核、拥有哪些特殊权力。然后我们试着在四个地方写入。
มันทำอะไร openshell sandbox exec -n demo -- <command> รันหนึ่งคำสั่งภายในแซนด์บ็อกซ์ชื่อ demo แล้วนำสิ่งที่มันพิมพ์กลับมา
ทุกอย่างหลังขีดสองขีดคือคำสั่ง เราถามว่าเราเป็นใคร อยู่ที่ไหน เคอร์เนลตัวไหนอยู่ข้างใต้ และเรามีอำนาจพิเศษอะไรบ้าง จากนั้นเราลองเขียนในสี่ที่
# look_inside.sh - who are you in there, and what can you touch?
openshell sandbox exec -n demo -- whoami
openshell sandbox exec -n demo -- pwd
openshell sandbox exec -n demo -- uname -r
openshell sandbox exec -n demo -- grep Cap /proc/self/status
# Two folders you may write in. One you may read but not change. One you may not open.
openshell sandbox exec -n demo -- sh -c 'echo hello > /sandbox/note.txt && cat /sandbox/note.txt'
openshell sandbox exec -n demo -- sh -c 'echo hello > /tmp/note.txt && cat /tmp/note.txt'
openshell sandbox exec -n demo -- head -1 /etc/os-release
openshell sandbox exec -n demo -- sh -c 'echo hello > /etc/note.txt'
openshell sandbox exec -n demo -- ls /home
# The rules that gave those answers.
openshell policy get demo --base$ openshell sandbox exec -n demo -- whoami
ubuntu
$ openshell sandbox exec -n demo -- pwd
/sandbox
$ openshell sandbox exec -n demo -- uname -r
6.12.76
$ openshell sandbox exec -n demo -- grep Cap /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
$ openshell sandbox exec -n demo -- sh -c 'echo hello > /sandbox/note.txt && cat /sandbox/note.txt'
hello
$ openshell sandbox exec -n demo -- sh -c 'echo hello > /tmp/note.txt && cat /tmp/note.txt'
hello
$ openshell sandbox exec -n demo -- head -1 /etc/os-release
PRETTY_NAME="Ubuntu 24.04.5 LTS"
$ openshell sandbox exec -n demo -- sh -c 'echo hello > /etc/note.txt'
sh: 1: cannot create /etc/note.txt: Permission denied
$ openshell sandbox exec -n demo -- ls /home
ls: cannot open directory '/home': Permission denied
$ openshell policy get demo --base
Version: 1
Hash: a45a8ff3564f00ae26cbd66f1131f37eedd8b54a66fbfec103d65a204c19c6e8
Status: Effective
Source: sandbox
Config rev: 16709790733110020355
---
version: 1
filesystem_policy:
include_workdir: true
read_only:
- /bin
- /usr
- /lib
- /proc
- /dev/urandom
- /etc
- /var/log
read_write:
- /tmp
- /dev/null
landlock:
compatibility: best_effortRead the output. You are an ordinary user called ubuntu, in the folder
/sandbox. The kernel is 6.12.76, not the
6.6.114.1-microsoft-standard-WSL2 of step 1: the sandbox really has a kernel of its own. The five
lines that start with Cap are all zeros. Those are the special powers Linux can give a
program, and this one has none. Writing works in /sandbox and in /tmp.
/etc can be read and cannot be changed. /home cannot even be opened. The
YAML at the end is the policy that gave those answers: compare its two lists with what just
happened. It has no section for the network at all, and that is the next step.
读输出。你是一个名叫 ubuntu 的普通用户,在 /sandbox 文件夹里。内核是 6.12.76,
不是第 1 步里的 6.6.114.1-microsoft-standard-WSL2:沙盒确实有自己的内核。以 Cap 开头的五行全是零。那是 Linux 能给程序的特殊权力,
而这个程序一项都没有。在 /sandbox 和 /tmp 里可以写入。/etc 可以读,不能改。/home 连打开都不行。
最后的 YAML 就是给出这些回答的策略:把它的两个列表和刚才发生的事对照一下。它完全没有关于网络的部分,那就是下一步。
อ่านผลลัพธ์ คุณเป็นผู้ใช้ธรรมดาชื่อ ubuntu อยู่ในโฟลเดอร์ /sandbox เคอร์เนลคือ 6.12.76
ไม่ใช่ 6.6.114.1-microsoft-standard-WSL2 ของขั้นที่ 1: แซนด์บ็อกซ์มีเคอร์เนลของตัวเองจริง ๆ ห้าบรรทัดที่ขึ้นต้นด้วย Cap เป็นศูนย์ทั้งหมด
นั่นคืออำนาจพิเศษที่ Linux ให้โปรแกรมได้ และโปรแกรมนี้ไม่มีเลย การเขียนทำได้ใน /sandbox และ /tmp ส่วน /etc อ่านได้แต่เปลี่ยนไม่ได้
/home เปิดไม่ได้เลย YAML ตอนท้ายคือนโยบายที่ให้คำตอบเหล่านั้น: เทียบสองรายการในนั้นกับสิ่งที่เพิ่งเกิดขึ้น มันไม่มีส่วนของเครือข่ายเลย และนั่นคือขั้นถัดไป
What it does. Inside the sandbox, curl asks GitHub for one line of advice. The
address api.github.com/zen needs no account, so it makes a good test. Then we read the
sandbox's log and keep the lines that have an arrow in them. grep -e '->' does that: grep
keeps the lines that contain a text, and -e tells it that the next word is the text,
even though it starts with a dash.
它做什么。在沙盒里,curl 向 GitHub 要一句箴言。api.github.com/zen 这个地址不需要账号,所以很适合做测试。
然后我们读沙盒的日志,只留下带箭头的行。grep -e '->' 做的就是这件事:grep 留下包含某段文字的行,-e 告诉它下一个词就是那段文字,
即使它以短横线开头。
มันทำอะไร ภายในแซนด์บ็อกซ์ curl ขอคำแนะนำหนึ่งบรรทัดจาก GitHub ที่อยู่ api.github.com/zen ไม่ต้องใช้บัญชี จึงเหมาะกับการทดสอบ
จากนั้นเราอ่านล็อกของแซนด์บ็อกซ์และเก็บเฉพาะบรรทัดที่มีลูกศร grep -e '->' ทำสิ่งนั้น: grep เก็บบรรทัดที่มีข้อความที่กำหนด และ -e
บอกว่าคำถัดไปคือข้อความนั้น แม้มันจะขึ้นต้นด้วยขีด
# try_network.sh - a new sandbox may talk to nobody.
openshell sandbox exec -n demo -- curl -sS https://api.github.com/zen
# Every refusal is written down. Give the log a moment, then read the lines
# with an arrow in them: who tried to reach what.
sleep 3
openshell logs demo --source sandbox | grep -e '->'$ openshell sandbox exec -n demo -- curl -sS https://api.github.com/zen
curl: (7) Failed to connect to api.github.com port 443 after 6 ms: Couldn't connect to server
$ sleep 3
$ openshell logs demo --source sandbox | grep -e '->'
[1791554949.941] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(0) -> api.github.com:443 [reason:transparent_tcp_policy_denied]Read the output. Couldn't connect to server, after a few thousandths of a second.
The request did not travel and come back with a no. It never left. And the log has one line for it:
NET:OPEN (a connection was being opened), DENIED, the program
/usr/bin/curl, the arrow, and the address api.github.com:443. Nobody wrote a
rule that forbids GitHub. There is simply no rule that allows it. That is default deny.
读输出。Couldn't connect to server,只用了千分之几秒。这个请求并不是出去之后带着“不行”回来的。它根本没有离开。
日志里有一行记录了它:NET:OPEN(正在打开一个连接)、DENIED、程序 /usr/bin/curl、箭头,以及地址 api.github.com:443。
没有人写过禁止 GitHub 的规则。只是没有任何规则允许它。这就是默认拒绝。
อ่านผลลัพธ์ Couldn't connect to server หลังจากเพียงไม่กี่ในพันของวินาที คำขอไม่ได้เดินทางออกไปแล้วกลับมาพร้อมคำว่าไม่ มันไม่เคยออกไปเลย
และล็อกมีหนึ่งบรรทัดสำหรับมัน: NET:OPEN (กำลังเปิดการเชื่อมต่อ) DENIED โปรแกรม /usr/bin/curl ลูกศร และที่อยู่ api.github.com:443
ไม่มีใครเขียนกฎห้าม GitHub เพียงแต่ไม่มีกฎข้อใดอนุญาต นั่นคือการปฏิเสธเป็นค่าเริ่มต้น
What it does. openshell policy update adds a rule to the running sandbox. The rule
has a name, github_api. It names one program, /usr/bin/curl. And it names
one endpoint, written as five words with colons between them:
api.github.com:443:read-only:rest:enforce. The host. The port. read-only:
only requests that read. rest: open each request and look at it. enforce:
stop a request that breaks the rule, instead of only writing it down. --wait waits until
the sandbox has loaded the rule. A backslash at the end of a line means that the command goes on.
它做什么。openshell policy update 给正在运行的沙盒加一条规则。这条规则有名字:github_api。它指定一个程序:
/usr/bin/curl。它还指定一个端点,写成用冒号隔开的五个词:api.github.com:443:read-only:rest:enforce。主机。端口。
read-only:只允许读取类的请求。rest:把每个请求打开来看。enforce:拦下违反规则的请求,而不是只记录下来。
--wait 会等到沙盒加载完这条规则。行尾的反斜杠表示命令还没写完。
มันทำอะไร openshell policy update เพิ่มกฎให้แซนด์บ็อกซ์ที่กำลังทำงาน กฎมีชื่อว่า github_api มันระบุโปรแกรมหนึ่งตัว คือ
/usr/bin/curl และระบุเอนด์พอยต์หนึ่งแห่ง เขียนเป็นห้าคำคั่นด้วยโคลอน: api.github.com:443:read-only:rest:enforce โฮสต์ พอร์ต
read-only: เฉพาะคำขอที่อ่าน rest: เปิดดูคำขอแต่ละรายการ enforce: หยุดคำขอที่ผิดกฎ ไม่ใช่แค่จดไว้
--wait รอจนแซนด์บ็อกซ์โหลดกฎเสร็จ เครื่องหมายแบ็กสแลชท้ายบรรทัดหมายความว่าคำสั่งยังไม่จบ
# allow_reading.sh - let one program read from one address.
openshell policy update demo \
--rule-name github_api \
--binary /usr/bin/curl \
--add-endpoint api.github.com:443:read-only:rest:enforce \
--wait
# Reading works now. Writing does not.
openshell sandbox exec -n demo -- curl -sS https://api.github.com/zen
openshell sandbox exec -n demo -- curl -sS -X POST https://api.github.com/zen
# The rule names curl. Python is still shut out, even from the same address.
openshell sandbox upload demo ask_github.py
openshell sandbox exec -n demo -- python3 ask_github.py
# The log again. Lines with an arrow are connections, HTTP lines are requests.
sleep 3
openshell logs demo --source sandbox | grep -e '->' -e 'HTTP:'The script also puts a small Python file into the sandbox with openshell sandbox upload
and runs it there. It asks the same address the same question:
这个脚本还用 openshell sandbox upload 把一个小的 Python 文件放进沙盒,并在里面运行它。它向同一个地址问同一个问题:
สคริปต์ยังใส่ไฟล์ Python เล็ก ๆ เข้าไปในแซนด์บ็อกซ์ด้วย openshell sandbox upload แล้วรันมันในนั้น มันถามคำถามเดียวกันกับที่อยู่เดียวกัน:
"""ask_github.py - ask GitHub for one line of advice. It runs inside the sandbox.
python3 ask_github.py
"""
import urllib.request
ADDRESS = "https://api.github.com/zen"
def main():
try:
with urllib.request.urlopen(ADDRESS, timeout=15) as reply:
print("GitHub says:", reply.read().decode())
except OSError as problem:
print("could not connect:", problem)
if __name__ == "__main__":
main()$ openshell policy update demo \
--rule-name github_api \
--binary /usr/bin/curl \
--add-endpoint api.github.com:443:read-only:rest:enforce \
--wait
✓ Policy version 2 submitted (hash: ec35d20221e1)
✓ Policy version 2 loaded (active version: 2)
$ openshell sandbox exec -n demo -- curl -sS https://api.github.com/zen
Anything added dilutes everything else.
$ openshell sandbox exec -n demo -- curl -sS -X POST https://api.github.com/zen
{"binary":"/usr/bin/curl","detail":"POST /zen not permitted by policy","error":"policy_denied","host":"api.github.com","layer":"l7","method":"POST","next_steps":[],"path":"/zen","policy":"github_api","port":443,"protocol":"rest","rule":"POST /zen","rule_missing":{"binary":"/usr/bin/curl","host":"api.github.com","layer":"l7","method":"POST","path":"/zen","port":443,"type":"rest_allow"}}
$ openshell sandbox upload demo ask_github.py
Uploading ask_github.py -> sandbox:~
✓ Upload complete
$ openshell sandbox exec -n demo -- python3 ask_github.py
could not connect: <urlopen error [Errno 13] Permission denied>
$ sleep 3
$ openshell logs demo --source sandbox | grep -e '->' -e 'HTTP:'
[1791554949.941] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(0) -> api.github.com:443 [reason:transparent_tcp_policy_denied]
[1791554959.269] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(0) -> api.github.com:443 [policy:github_api engine:opa]
[1791554959.323] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com:443/zen [policy:github_api engine:l7]
[1791554959.504] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(0) -> api.github.com:443 [policy:github_api engine:opa]
[1791554959.554] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com:443/zen [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/zen reason=POST /zen not permitted by policy]
[1791554959.907] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/python3.12(0) -> api.github.com:443 [reason:transparent_tcp_policy_denied]Read the output. Four results, and each one teaches something. 读输出。四个结果,每一个都说明一件事。 อ่านผลลัพธ์ ผลลัพธ์สี่อย่าง และแต่ละอย่างสอนหนึ่งเรื่อง
Policy version 2 loaded. Nothing was restarted.
规则是在沙盒运行时加载的。Policy version 2 loaded。什么都没有重启。
กฎถูกโหลดขณะที่แซนด์บ็อกซ์ทำงานอยู่ Policy version 2 loaded ไม่มีอะไรถูกเริ่มใหม่POST is a block of JSON
with "error":"policy_denied" and "layer":"l7". The supervisor wrote that
answer itself. The request never reached GitHub.
写入被拒绝,而且拒绝它的不是 GitHub。对 POST 的回答是一段 JSON,里面有 "error":"policy_denied" 和 "layer":"l7"。
这个回答是监督程序自己写的。请求从来没有到达 GitHub。
การเขียนถูกปฏิเสธ และผู้ปฏิเสธไม่ใช่ GitHub คำตอบของ POST เป็นก้อน JSON ที่มี "error":"policy_denied" และ "layer":"l7"
ซูเปอร์ไวเซอร์เขียนคำตอบนั้นเอง คำขอไม่เคยไปถึง GitHubPermission denied. In the log, the program is /usr/bin/python3.12: the real
file behind the name python3. A rule has to use that real name.
规则指定的是程序,不只是地址。Python 访问同一个地址,得到 Permission denied。在日志里,程序是 /usr/bin/python3.12:
python3 这个名字背后真正的文件。规则必须使用这个真实的名字。
กฎระบุโปรแกรม ไม่ใช่แค่ที่อยู่ Python ถามที่อยู่เดียวกันและได้ Permission denied ในล็อก โปรแกรมคือ /usr/bin/python3.12:
ไฟล์จริงที่อยู่หลังชื่อ python3 กฎต้องใช้ชื่อจริงนั้นChange it. Run openshell policy get demo --base and find your rule in the YAML,
under network_policies. Then open machine 2 in Part 3 and press “one rule (step 9)”: it
shows these same four results.
改一改。运行 openshell policy get demo --base,在 YAML 的 network_policies 下面找到你的规则。
然后打开第 3 部分的第 2 台机器,按“one rule (step 9)”:它显示的就是这四个结果。
ลองเปลี่ยน รัน openshell policy get demo --base แล้วหากฎของคุณใน YAML ใต้ network_policies
จากนั้นเปิดเครื่องที่ 2 ในส่วนที่ 3 แล้วกด “one rule (step 9)”: มันแสดงผลลัพธ์สี่อย่างเดียวกันนี้
What it does. When OpenShell refuses a connection, it also writes a proposal: a rule
that would allow exactly that program to reach exactly that address. A proposal changes nothing until
a person decides. openshell rule get demo --status pending lists the proposals that are
waiting. rule approve-all approves every one of them, so we use it only when the list
has one entry and we have read it. rule clear throws the waiting proposals away. The
sleep 12 lines are there because the sandbox reports to the gateway, and fetches new
rules, about every ten seconds.
它做什么。当 OpenShell 拒绝一个连接时,它还会写一份提议:一条恰好允许那个程序访问那个地址的规则。在有人做出决定之前,提议什么都不改变。
openshell rule get demo --status pending 列出正在等待的提议。rule approve-all 会批准其中的每一个,
所以我们只在列表里只有一项、而且我们已经读过它的时候才用。rule clear 把等待中的提议全部丢掉。那几行 sleep 12
是因为沙盒大约每十秒向网关报告一次,并取回新的规则。
มันทำอะไร เมื่อ OpenShell ปฏิเสธการเชื่อมต่อ มันจะเขียนข้อเสนอด้วย: กฎที่จะอนุญาตให้โปรแกรมนั้นเข้าถึงที่อยู่นั้นพอดี ข้อเสนอไม่เปลี่ยนอะไรจนกว่าคนจะตัดสินใจ
openshell rule get demo --status pending แสดงรายการข้อเสนอที่รออยู่ rule approve-all อนุมัติทุกรายการ เราจึงใช้มันเฉพาะเมื่อรายการมีหนึ่งข้อและเราอ่านแล้ว
rule clear ทิ้งข้อเสนอที่รออยู่ บรรทัด sleep 12 มีไว้เพราะแซนด์บ็อกซ์รายงานต่อเกตเวย์และรับกฎใหม่ประมาณทุกสิบวินาที
# review.sh - OpenShell writes a proposal for what it refused. You decide.
# A proposal reaches the gateway with the sandbox's next report. Read it.
sleep 12
openshell rule get demo --status pending
# There is one proposal, and you have read it: approve it.
openshell rule approve-all demo
# The sandbox picks up a new rule within about ten seconds. Try again.
sleep 12
openshell sandbox exec -n demo -- python3 ask_github.py
# Another address is still closed, and OpenShell proposes to open that too.
openshell sandbox exec -n demo -- curl -sS https://example.com/
sleep 12
openshell rule get demo --status pending
# This one you do not want. Throw the pending proposals away.
openshell rule clear demo
openshell policy list demo$ sleep 12
$ openshell rule get demo --status pending
Network Rules: (version 2, 1 chunk)
Chunk: 16fe9474-9d6d-4881-a43d-2ced629f5628
Status: pending
Rule: allow_api_github_com_443
Binary: /usr/bin/python3.12
Confidence: 65%
Rationale: Allow python3.12 to connect to api.github.com:443 (HTTPS).
Prover: prover: no new findings
Candidate: 61c22f07956b
Endpoints: api.github.com:443 [L7 rest, access=read-only]
Binaries: /usr/bin/python3.12
$ openshell rule approve-all demo
OK 1 chunk(s) approved, 0 skipped. Policy version: 3
$ sleep 12
$ openshell sandbox exec -n demo -- python3 ask_github.py
GitHub says: Non-blocking is better than blocking.
$ openshell sandbox exec -n demo -- curl -sS https://example.com/
curl: (7) Failed to connect to example.com port 443 after 3 ms: Couldn't connect to server
$ sleep 12
$ openshell rule get demo --status pending
Network Rules: (version 3, 1 chunk)
Chunk: 852120e1-1c1d-45f3-a08a-fd570623785f
Status: pending
Rule: allow_example_com_443
Binary: /usr/bin/curl
Confidence: 65%
Rationale: Allow curl to connect to example.com:443 (HTTPS).
Prover: prover: no new findings
Candidate: 09b39669fb21
Endpoints: example.com:443 [L4]
Binaries: /usr/bin/curl
$ openshell rule clear demo
OK 1 pending chunk(s) cleared.
$ openshell policy list demo
VERSION HASH STATUS CREATED ERROR
3 61c22f07956b Loaded 1791554975126
2 ec35d20221e1 Superseded 1791554953106
1 a45a8ff3564f Superseded 1791554948696 Read the output. The first list has one proposal, from the Python file that was refused in
step 9. Read it as a reviewer would. Binary: which program. Endpoints: which
address, and access=read-only. Prover: no new findings: OpenShell compared
what the sandbox could reach with and without this rule and found none of the risks it looks for.
We approve it, wait, and Python gets its line from GitHub. Then curl tries
example.com, is refused, and a proposal for that appears too. We do not want it, so we
clear it. The last table shows three versions of the policy, and the sandbox is on the newest.
读输出。第一个列表里有一份提议,来自第 9 步被拒绝的那个 Python 文件。像审核人一样读它。Binary:哪个程序。Endpoints:哪个地址,
以及 access=read-only。Prover: no new findings:OpenShell 比较了有这条规则和没有这条规则时沙盒能访问的范围,没有发现它要找的任何风险。
我们批准它,等一等,Python 就从 GitHub 拿到了那一句。接着 curl 尝试访问 example.com,被拒绝,于是又出现了一份针对它的提议。
我们不想要它,所以清掉。最后的表格显示策略有三个版本,沙盒用的是最新的。
อ่านผลลัพธ์ รายการแรกมีข้อเสนอหนึ่งข้อ มาจากไฟล์ Python ที่ถูกปฏิเสธในขั้นที่ 9 อ่านมันอย่างผู้ตรวจ Binary: โปรแกรมไหน Endpoints: ที่อยู่ไหน
และ access=read-only Prover: no new findings: OpenShell เทียบสิ่งที่แซนด์บ็อกซ์เข้าถึงได้เมื่อมีและไม่มีกฎนี้ และไม่พบความเสี่ยงที่มันมองหา
เราอนุมัติ รอ แล้ว Python ก็ได้บรรทัดของมันจาก GitHub จากนั้น curl ลองไปที่ example.com ถูกปฏิเสธ และข้อเสนอสำหรับเรื่องนั้นก็ปรากฏขึ้นด้วย
เราไม่ต้องการมัน จึงล้างทิ้ง ตารางสุดท้ายแสดงนโยบายสามเวอร์ชัน และแซนด์บ็อกซ์ใช้เวอร์ชันใหม่สุด
With more than one proposal, do not use approve-all. Copy the Chunk id of
the one you want and approve only that:
openshell rule approve demo --chunk-id <the id>. To refuse one and say why:
openshell rule reject demo --chunk-id <the id> --reason "…". We did not record those
two commands in a step, because the id is different on every computer.
如果提议不止一份,不要用 approve-all。复制你想要的那份的 Chunk 编号,只批准它:
openshell rule approve demo --chunk-id <the id>。要拒绝一份并说明原因:
openshell rule reject demo --chunk-id <the id> --reason "…"。这两条命令我们没有记录在步骤里,因为编号在每台电脑上都不一样。
ถ้ามีข้อเสนอมากกว่าหนึ่งข้อ อย่าใช้ approve-all ให้คัดลอกรหัส Chunk ของข้อที่คุณต้องการแล้วอนุมัติเฉพาะข้อนั้น:
openshell rule approve demo --chunk-id <the id> ถ้าจะปฏิเสธข้อหนึ่งพร้อมบอกเหตุผล:
openshell rule reject demo --chunk-id <the id> --reason "…" เราไม่ได้บันทึกสองคำสั่งนี้ไว้ในขั้นตอน เพราะรหัสต่างกันในคอมพิวเตอร์ทุกเครื่อง
What it does. A policy is data, so a program can read it. The first command saves the
sandbox's rules as a JSON file. Then decide.py reads that file and answers four requests
the way the sandbox would. endpoints_for() is check 1: it walks through the rules and
keeps the endpoints that list this program with this address. lets_through() is check 2:
it looks at the endpoint's access word. decide() puts the two together. This
is a model for learning, and it knows far less than OpenShell does.
它做什么。策略是数据,所以程序可以读它。第一条命令把沙盒的规则存成一个 JSON 文件。然后 decide.py 读这个文件,
并像沙盒那样回答四个请求。endpoints_for() 是第一次检查:它把规则走一遍,留下那些同时列出这个程序和这个地址的端点。
lets_through() 是第二次检查:它看端点的 access 这个词。decide() 把两者合在一起。这是一个用来学习的模型,
它懂的远比 OpenShell 少。
มันทำอะไร นโยบายคือข้อมูล โปรแกรมจึงอ่านมันได้ คำสั่งแรกบันทึกกฎของแซนด์บ็อกซ์เป็นไฟล์ JSON จากนั้น decide.py อ่านไฟล์นั้น
และตอบคำขอสี่รายการแบบที่แซนด์บ็อกซ์จะตอบ endpoints_for() คือการตรวจครั้งที่ 1: มันไล่ดูกฎและเก็บเอนด์พอยต์ที่ระบุโปรแกรมนี้คู่กับที่อยู่นี้
lets_through() คือการตรวจครั้งที่ 2: มันดูคำ access ของเอนด์พอยต์ decide() นำทั้งสองมารวมกัน นี่คือแบบจำลองสำหรับเรียนรู้
และมันรู้น้อยกว่า OpenShell มาก
"""decide.py - the two checks a sandbox makes, as a small model in Python.
openshell policy get demo --base -o json > policy.json
python3 decide.py policy.json
It reads the sandbox's real rules and says what should happen to four
requests. It is a model for learning, not OpenShell's own code: it knows exact
addresses and the three access words, and nothing else.
"""
import json
import sys
METHODS = {
"read-only": {"GET", "HEAD", "OPTIONS"},
"read-write": {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"},
}
REQUESTS = [
("/usr/bin/curl", "api.github.com", 443, "GET"),
("/usr/bin/curl", "api.github.com", 443, "POST"),
("/usr/bin/curl", "example.com", 443, "GET"),
("/usr/bin/python3.12", "api.github.com", 443, "GET"),
]
def endpoints_for(rules, program, host, port):
"""Check 1, the connection: the endpoints that let this program reach this address."""
found = []
for rule in rules.values():
programs = [binary["path"] for binary in rule["binaries"]]
if program not in programs:
continue
for endpoint in rule["endpoints"]:
if endpoint["host"] == host and endpoint["port"] == port:
found.append(endpoint)
return found
def lets_through(endpoint, method):
"""Check 2, the request: does this endpoint let this method through?"""
if "protocol" not in endpoint:
return True # nobody reads the requests on this endpoint
if endpoint.get("enforcement") != "enforce":
return True # "audit" writes a bad request down, and lets it pass
access = endpoint.get("access")
return access == "full" or method in METHODS.get(access, set())
def decide(rules, program, host, port, method):
"""What happens to one request: the verdict, and the check that gave it."""
endpoints = endpoints_for(rules, program, host, port)
if not endpoints:
return "DENIED", "check 1: no rule lists this program with this address"
if any(lets_through(endpoint, method) for endpoint in endpoints):
return "ALLOWED", "both checks passed"
return "DENIED", f"check 2: {method} is not in this endpoint's access"
def main():
with open(sys.argv[1]) as file:
rules = json.load(file)["policy"].get("network_policies", {})
print(len(rules), "rules:", ", ".join(rules))
for program, host, port, method in REQUESTS:
verdict, why = decide(rules, program, host, port, method)
print(f"{verdict:8}{method:5}{program} -> {host}:{port} {why}")
if __name__ == "__main__":
main()# predict.sh - save the sandbox's rules as a file, and let a small model read them.
openshell policy get demo --base -o json > policy.json
python3 decide.py policy.json$ openshell policy get demo --base -o json > policy.json
$ python3 decide.py policy.json
2 rules: allow_api_github_com_443, github_api
ALLOWED GET /usr/bin/curl -> api.github.com:443 both checks passed
DENIED POST /usr/bin/curl -> api.github.com:443 check 2: POST is not in this endpoint's access
DENIED GET /usr/bin/curl -> example.com:443 check 1: no rule lists this program with this address
ALLOWED GET /usr/bin/python3.12 -> api.github.com:443 both checks passedRead the output. Two rules, and four answers. Compare them with what really happened: the
GET from curl worked in step 9, the POST was refused in step 9,
example.com was refused in step 10, and Python worked in step 10 after the approval.
Four out of four. One line in the file deserves a second look:
if endpoint.get("enforcement") != "enforce". According to OpenShell's manual, an endpoint
that does not say enforce is in audit mode: it writes a bad request down and lets
it pass. We did not test audit mode.
读输出。两条规则,四个回答。把它们和真实发生的事对照:curl 的 GET 在第 9 步成功,POST 在第 9 步被拒绝,
example.com 在第 10 步被拒绝,Python 在第 10 步批准之后成功。四个全对。文件里有一行值得再看一眼:
if endpoint.get("enforcement") != "enforce"。按照 OpenShell 的手册,没有写 enforce 的端点处于审计模式:
它把不合规的请求记录下来,然后放行。审计模式我们没有测试。
อ่านผลลัพธ์ กฎสองข้อ และคำตอบสี่ข้อ เทียบกับสิ่งที่เกิดขึ้นจริง: GET จาก curl สำเร็จในขั้นที่ 9 POST ถูกปฏิเสธในขั้นที่ 9
example.com ถูกปฏิเสธในขั้นที่ 10 และ Python สำเร็จในขั้นที่ 10 หลังการอนุมัติ ถูกทั้งสี่ข้อ มีหนึ่งบรรทัดในไฟล์ที่ควรดูอีกครั้ง:
if endpoint.get("enforcement") != "enforce" ตามคู่มือของ OpenShell เอนด์พอยต์ที่ไม่ได้ระบุ enforce อยู่ในโหมดตรวจสอบ (audit):
มันจดคำขอที่ผิดกฎไว้แล้วปล่อยให้ผ่าน เราไม่ได้ทดสอบโหมด audit
What it does. An agent usually needs a secret key to do its job, and a key in an agent's hands can be leaked. OpenShell's answer is the provider: you hand the key to the gateway, and the sandbox gets a placeholder. To watch that happen without a real account, we make a pretend API. It runs on your own computer, outside the sandbox, and gives its notes only to the right key. It prints every key it is shown in its own window. 它做什么。智能体通常需要一把秘密钥匙才能完成工作,而落在智能体手里的钥匙可能被泄露。OpenShell 的回答是 provider(提供方): 你把钥匙交给网关,沙盒得到的是一个占位符。为了不用真实账号就能看到这个过程,我们做一个假的 API。它运行在你自己的电脑上、沙盒外面, 只把笔记交给正确的钥匙。它把自己看到的每一把钥匙都打印在自己的窗口里。 มันทำอะไร agent มักต้องใช้กุญแจลับเพื่อทำงาน และกุญแจในมือของ agent อาจรั่วไหลได้ คำตอบของ OpenShell คือ provider: คุณมอบกุญแจให้เกตเวย์ และแซนด์บ็อกซ์ได้ตัวยึดตำแหน่ง เพื่อดูสิ่งนี้เกิดขึ้นโดยไม่ต้องมีบัญชีจริง เราสร้าง API ปลอม มันทำงานบนคอมพิวเตอร์ของคุณเอง นอกแซนด์บ็อกซ์ และให้โน้ตเฉพาะกับกุญแจที่ถูกต้อง มันพิมพ์กุญแจทุกดอกที่เห็นไว้ในหน้าต่างของตัวเอง
"""key_server.py - a pretend API that only answers the right key.
python3 key_server.py
It listens on this computer only, on port 8642, answers two requests and
stops. It never sends a key back. It prints each key it was shown in its own
window, where a program inside the sandbox cannot look.
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT = 8642
RIGHT_KEY = "Bearer kru-1234-this-is-the-real-key"
REQUESTS = 2
class NotesApi(BaseHTTPRequestHandler):
"""The standard library calls do_GET for every GET request that arrives."""
def do_GET(self):
key = self.headers.get("Authorization", "(no key)")
print("a request for", self.path, "came with:", key, flush=True)
if key == RIGHT_KEY:
self.answer(200, "The right key. Your notes: buy khao soi.\n")
else:
self.answer(401, "Wrong key. No notes for you.\n")
def answer(self, status, text):
self.send_response(status)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(text.encode())
def log_message(self, *arguments):
"""Print nothing more: do_GET already printed what matters."""
def main():
server = HTTPServer(("127.0.0.1", PORT), NotesApi)
print("pretend API listening on port", PORT, flush=True)
for _ in range(REQUESTS):
server.handle_request()
print("answered", REQUESTS, "requests, stopping", flush=True)
if __name__ == "__main__":
main()A provider profile tells OpenShell about a service: the name of the variable that will hold
the key, how the key travels (bearer means in a line called Authorization),
the one endpoint where the key may go, and the one program that may take it there.
host.openshell.internal is OpenShell's name for “the computer the gateway runs on”.
provider profile(提供方配置)向 OpenShell 描述一个服务:保存钥匙的变量叫什么名字、钥匙怎样传递(bearer 表示放在名为
Authorization 的一行里)、钥匙唯一可以去的端点,以及唯一可以把它带过去的程序。host.openshell.internal 是 OpenShell 给“网关所在的这台电脑”起的名字。
provider profile บอก OpenShell เกี่ยวกับบริการหนึ่ง: ชื่อของตัวแปรที่จะเก็บกุญแจ กุญแจเดินทางอย่างไร (bearer หมายถึงอยู่ในบรรทัดชื่อ Authorization)
เอนด์พอยต์เดียวที่กุญแจไปได้ และโปรแกรมเดียวที่นำมันไปที่นั่นได้ host.openshell.internal คือชื่อที่ OpenShell ใช้เรียก “คอมพิวเตอร์ที่เกตเวย์ทำงานอยู่”
# notes_api.yaml - a provider profile for the pretend API in key_server.py.
# It says: this key may go to one address only, and only Python may take it there.
id: notes-api
display_name: Notes API
description: A pretend API on this computer, for watching where a key goes
category: other
credentials:
- name: api_key
description: The key of the pretend API
env_vars: [NOTES_API_KEY]
required: true
auth_style: bearer
header_name: authorization
endpoints:
- host: host.openshell.internal
port: 8642
protocol: rest
access: read-only
enforcement: enforce
binaries: [/usr/bin/python3.12]The program in the sandbox stands in for an agent. It reads the key from its variable, shows what it holds, and sends it. Give it an address, and it sends the same key there. 沙盒里的这个程序代替智能体。它从变量里读出钥匙,显示自己拿到的是什么,然后把它发出去。给它一个地址,它就把同一把钥匙发到那里。 โปรแกรมในแซนด์บ็อกซ์ทำหน้าที่แทน agent มันอ่านกุญแจจากตัวแปร แสดงสิ่งที่มันถืออยู่ แล้วส่งออกไป ถ้าให้ที่อยู่ มันจะส่งกุญแจเดียวกันไปที่นั่น
"""fetch_notes.py - a small stand-in for an agent. It runs inside the sandbox.
python3 fetch_notes.py ask the pretend API for the notes
python3 fetch_notes.py <an address> send the same key somewhere else
It knows the name of the variable that holds the key. Watch what is in it.
"""
import os
import sys
import urllib.error
import urllib.request
NOTES = "http://host.openshell.internal:8642/notes"
def main():
address = sys.argv[1] if len(sys.argv) > 1 else NOTES
key = os.environ["NOTES_API_KEY"]
print("the key I hold:", key)
request = urllib.request.Request(address, headers={"Authorization": "Bearer " + key})
try:
with urllib.request.urlopen(request, timeout=15) as reply:
print("the answer :", reply.read().decode().strip())
except urllib.error.HTTPError as refusal:
print("refused :", refusal.code, refusal.read().decode().strip())
if __name__ == "__main__":
main()# keys.sh - give the sandbox a key that it can use but cannot read.
# 1. Describe the pretend API to the gateway, and hand over its key.
openshell profile import -f notes_api.yaml
NOTES_API_KEY=kru-1234-this-is-the-real-key \
openshell provider create --name notes --type notes-api --credential NOTES_API_KEY
# 2. Start the pretend API here, outside the sandbox. It writes what it sees to a file.
python3 key_server.py > key_server_seen.txt 2>&1 &
# 3. Attach the key to the running sandbox, and put the small agent inside.
openshell sandbox provider attach demo notes --wait
openshell sandbox upload demo fetch_notes.py
# 4. Inside the sandbox: what is the key, and does it work?
openshell sandbox exec -n demo -- python3 fetch_notes.py
# 5. From out here, with a guess for a key. The pretend API is strict.
curl -sS -H "Authorization: Bearer my-guess" http://127.0.0.1:8642/notes
# 6. Outside the sandbox: what did the pretend API see?
cat key_server_seen.txt
# 7. Inside again: carry the same key to another address that Python may reach.
openshell sandbox exec -n demo -- python3 fetch_notes.py https://api.github.com/zen$ openshell profile import -f notes_api.yaml
Imported 1 provider profile.
$ NOTES_API_KEY=kru-1234-this-is-the-real-key \
openshell provider create --name notes --type notes-api --credential NOTES_API_KEY
✓ Created provider notes
$ python3 key_server.py > key_server_seen.txt 2>&1 &
$ openshell sandbox provider attach demo notes --wait
demo / notes: ready (unspecified)
Receipt: 07060bf1-2bb7-4015-aab1-10545efb32ae
Persisted: 2026-10-09T14:10:00.034124554Z
Installed: credentials=true, policy=true, future process environment=true
Wait: complete
$ openshell sandbox upload demo fetch_notes.py
Uploading fetch_notes.py -> sandbox:~
✓ Upload complete
$ openshell sandbox exec -n demo -- python3 fetch_notes.py
the key I hold: openshell:resolve:env:v10425789863009201859_NOTES_API_KEY
the answer : The right key. Your notes: buy khao soi.
$ curl -sS -H "Authorization: Bearer my-guess" http://127.0.0.1:8642/notes
Wrong key. No notes for you.
$ cat key_server_seen.txt
pretend API listening on port 8642
a request for /notes came with: Bearer kru-1234-this-is-the-real-key
a request for /notes came with: Bearer my-guess
answered 2 requests, stopping
$ openshell sandbox exec -n demo -- python3 fetch_notes.py https://api.github.com/zen
the key I hold: openshell:resolve:env:v10425789863009201859_NOTES_API_KEY
refused : 403 {"error":"credential_endpoint_mismatch","message":"Credential is not authorized for this request endpoint"}Read the output. Three things to see, and they are the heart of this page. 读输出。要看三件事,它们是这个页面的核心。 อ่านผลลัพธ์ มีสามสิ่งให้ดู และมันคือหัวใจของหน้านี้
openshell:resolve:env:…_NOTES_API_KEY. That is the placeholder.
在沙盒里,钥匙并不是钥匙。程序打印出它拿到的东西:openshell:resolve:env:…_NOTES_API_KEY。那是占位符。
ภายในแซนด์บ็อกซ์ กุญแจไม่ใช่กุญแจ โปรแกรมพิมพ์สิ่งที่มันถือ: openshell:resolve:env:…_NOTES_API_KEY นั่นคือตัวยึดตำแหน่งThe right key, and the API's own
record, printed by cat key_server_seen.txt, shows
Bearer kru-1234-this-is-the-real-key. The swap happened on the way out. Our guess from
outside the sandbox got Wrong key, so the API really does check.
API 收到的仍然是真正的钥匙。回答是 The right key,而 API 自己的记录(由 cat key_server_seen.txt 打印)显示
Bearer kru-1234-this-is-the-real-key。替换是在出去的路上发生的。我们在沙盒外面用猜的钥匙得到的是 Wrong key,可见这个 API 确实会检查。
API ยังได้รับกุญแจจริง คำตอบคือ The right key และบันทึกของ API เอง ซึ่งพิมพ์โดย cat key_server_seen.txt แสดง
Bearer kru-1234-this-is-the-real-key การสลับเกิดขึ้นระหว่างทางออก ส่วนการเดาของเราจากนอกแซนด์บ็อกซ์ได้ Wrong key แสดงว่า API ตรวจจริง403, credential_endpoint_mismatch. A stolen placeholder is worth nothing.
钥匙不能被带到别的任何地方。最后一条命令把同一把钥匙发给 GitHub,而 GitHub 是从第 10 步起 Python 被允许访问的地址。监督程序拒绝了:
403,credential_endpoint_mismatch。偷走占位符毫无用处。
กุญแจไม่สามารถถูกนำไปที่อื่นได้ คำสั่งสุดท้ายส่งกุญแจเดียวกันไปที่ GitHub ซึ่งเป็นที่อยู่ที่ Python ได้รับอนุญาตให้เข้าถึงตั้งแต่ขั้นที่ 10 ซูเปอร์ไวเซอร์ปฏิเสธ:
403, credential_endpoint_mismatch ตัวยึดตำแหน่งที่ถูกขโมยไปไม่มีค่าอะไรเลยWhat it does. OpenShell has a Python library with the same name. It talks to the gateway, as
the openshell command does, so a program of yours can make sandboxes, run things in them
and remove them. First a Python of your own, a venv, with the library in it. On Ubuntu the
venv tool is a separate package, and without it python3 -m venv stops with an error.
它做什么。OpenShell 有一个同名的 Python 库。它和 openshell 命令一样与网关对话,所以你自己的程序可以创建沙盒、在里面运行东西、再把它删除。
先要有一个属于你自己的 Python,也就是 venv,里面装着这个库。在 Ubuntu 上,venv 工具是一个单独的软件包,没有它 python3 -m venv 会报错停下。
มันทำอะไร OpenShell มีไลบรารี Python ชื่อเดียวกัน มันคุยกับเกตเวย์เหมือนที่คำสั่ง openshell ทำ โปรแกรมของคุณจึงสร้างแซนด์บ็อกซ์ รันสิ่งต่าง ๆ ในนั้น และลบมันได้
ก่อนอื่นต้องมี Python ของคุณเอง คือ venv ที่มีไลบรารีอยู่ข้างใน บน Ubuntu เครื่องมือ venv เป็นแพ็กเกจแยก และถ้าไม่มี python3 -m venv จะหยุดพร้อมข้อผิดพลาด
# make_venv.sh - a Python of your own, with the openshell library in it.
# On Ubuntu the venv tool is a separate package.
sudo apt-get install -y python3-venv
python3 -m venv ~/openshell-venv
~/openshell-venv/bin/pip install openshellWhat that printed is long: the whole log is here.
Its last line shows the library, openshell-0.1.2. Now the file.
SandboxClient.from_active_cluster() finds the gateway that the openshell
command uses. with … as client: closes the connection when the block ends.
create, wait_ready, exec and delete do what their
names say, and every one of them is told which workspace to use: a gateway can keep several
groups of sandboxes apart, and the one you get at the start is called default.
那几条命令打印的内容很长:完整的日志在这里。它的最后一行显示了这个库:
openshell-0.1.2。现在看文件。SandboxClient.from_active_cluster() 会找到 openshell 命令所用的那个网关。
with … as client: 在代码块结束时关闭连接。create、wait_ready、exec 和 delete 做的事和名字一样,
而且每一个都要被告知使用哪个 workspace(工作区):一个网关可以把几组沙盒分开保存,一开始给你的那个叫 default。
สิ่งที่คำสั่งเหล่านั้นพิมพ์ยาวมาก: ล็อกทั้งหมดอยู่ที่นี่ บรรทัดสุดท้ายแสดงไลบรารี
openshell-0.1.2 ตอนนี้มาดูไฟล์ SandboxClient.from_active_cluster() หาเกตเวย์ที่คำสั่ง openshell ใช้อยู่
with … as client: ปิดการเชื่อมต่อเมื่อบล็อกจบ create, wait_ready, exec และ delete ทำตามชื่อของมัน
และทุกตัวต้องได้รับการบอกว่าจะใช้ workspace ไหน: เกตเวย์เก็บแซนด์บ็อกซ์หลายกลุ่มแยกจากกันได้ และกลุ่มที่คุณได้ตอนเริ่มชื่อ default
"""hello_sdk.py - from Python: make a sandbox, run one command in it, remove it.
python hello_sdk.py
Use the Python that has the openshell library. The library finds the gateway
the same way the openshell command does, so the gateway must be running.
"""
from openshell import SandboxClient
WORKSPACE = "default"
NAME = "from-python"
def main():
# "with" closes the connection to the gateway when the block ends.
with SandboxClient.from_active_cluster() as client:
print("gateway version:", client.health().version)
client.create(workspace=WORKSPACE, name=NAME)
client.wait_ready(NAME, workspace=WORKSPACE, timeout_seconds=120)
result = client.exec(NAME, ["id"], workspace=WORKSPACE)
print("the sandbox says:", result.stdout.strip())
print("exit code:", result.exit_code)
deletion = client.delete(NAME, workspace=WORKSPACE)
client.wait_deleted(NAME, workspace=WORKSPACE,
expected_sandbox_id=deletion.sandbox_id)
print("sandbox removed")
if __name__ == "__main__":
main()# use_python.sh - run hello_sdk.py with the Python that has the openshell library.
~/openshell-venv/bin/python hello_sdk.py$ ~/openshell-venv/bin/python hello_sdk.py
gateway version: 0.1.2
the sandbox says: uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
exit code: 0
sandbox removedRead the output. A sandbox was made, answered one command and was removed, and nobody typed an
openshell command. The command we ran inside is id, because this sandbox
uses OpenShell's bare image, which has no Python in it.
Change it. Replace ["id"] with ["uname", "-r"] and run the file again.
读输出。一个沙盒被创建、回答了一条命令、又被删除,而没有人输入过 openshell 命令。我们在里面运行的命令是 id,
因为这个沙盒用的是 OpenShell 那个最精简的镜像,里面没有 Python。改一改。把 ["id"] 换成 ["uname", "-r"],再运行一次这个文件。
อ่านผลลัพธ์ แซนด์บ็อกซ์หนึ่งอันถูกสร้าง ตอบหนึ่งคำสั่ง และถูกลบ โดยไม่มีใครพิมพ์คำสั่ง openshell เลย คำสั่งที่เรารันข้างในคือ id
เพราะแซนด์บ็อกซ์นี้ใช้อิมเมจเปล่า ๆ ของ OpenShell ซึ่งไม่มี Python ลองเปลี่ยน แทน ["id"] ด้วย ["uname", "-r"] แล้วรันไฟล์อีกครั้ง
What it does. It removes the sandbox, then the provider, then the profile, in that order: a profile cannot be removed while a provider still uses it. 它做什么。它依次删除沙盒、provider、profile,顺序不能乱:只要还有 provider 在用,profile 就删不掉。 มันทำอะไร มันลบแซนด์บ็อกซ์ แล้วลบ provider แล้วลบ profile ตามลำดับนี้: profile ลบไม่ได้ขณะที่ยังมี provider ใช้มันอยู่
# clean_up.sh - remove what the lesson made.
openshell sandbox delete demo
openshell provider delete notes
openshell profile delete notes-api
openshell sandbox list$ openshell sandbox delete demo
✓ Deleted sandbox demo
$ openshell provider delete notes
✓ Deleted provider notes
$ openshell profile delete notes-api
✓ Deleted provider profile notes-api
$ openshell sandbox list
No sandboxes found.To remove OpenShell itself, its manual gives three commands for Ubuntu:
systemctl --user disable --now openshell-gateway, then
sudo apt remove openshell, then
rm -rf ~/.local/state/openshell. We did not run those three. We removed our whole lab
another way, because it lived in a WSL distro of its own: wsl --unregister openshell-lab.
要卸载 OpenShell 本身,它的手册为 Ubuntu 给出了三条命令:systemctl --user disable --now openshell-gateway,
然后 sudo apt remove openshell,再然后 rm -rf ~/.local/state/openshell。这三条我们没有运行。我们用另一种方式删除了整个实验环境,
因为它装在一个单独的 WSL 发行版里:wsl --unregister openshell-lab。
ถ้าจะลบ OpenShell เอง คู่มือของมันให้สามคำสั่งสำหรับ Ubuntu: systemctl --user disable --now openshell-gateway
แล้ว sudo apt remove openshell แล้ว rm -rf ~/.local/state/openshell เราไม่ได้รันสามคำสั่งนั้น เราลบห้องทดลองทั้งหมดด้วยอีกวิธีหนึ่ง
เพราะมันอยู่ใน WSL distro ของตัวเอง: wsl --unregister openshell-lab
Three things surprised us on the way. On our laptop the usual runtime could not start a sandbox, and we found the cause. A file uploaded from the Windows drive arrived full of zeros. And the tool's own check said that everything was fine. None of these is hidden: the proof is in the boxes below. The section ends with a list of what we did not test, so that you know how far to trust this page.
一路上有三件事让我们意外。在我们的笔记本上,通常的运行时启动不了沙盒,我们找到了原因。从 Windows 盘上传的文件,到达时里面全是零。而工具自己的检查却说一切正常。这些都没有藏起来:证据就在下面的方框里。这一部分最后列出了我们没有测试的东西,好让你知道这个页面可以信到什么程度。
ระหว่างทางมีสามเรื่องที่ทำให้เราแปลกใจ บนแล็ปท็อปของเรา รันไทม์ปกติเริ่มแซนด์บ็อกซ์ไม่ได้ และเราหาสาเหตุเจอ ไฟล์ที่อัปโหลดจากไดรฟ์ของ Windows ไปถึงโดยมีแต่เลขศูนย์ และการตรวจของเครื่องมือเองกลับบอกว่าทุกอย่างปกติ ไม่มีเรื่องไหนถูกซ่อนไว้: หลักฐานอยู่ในกล่องด้านล่าง ส่วนนี้จบด้วยรายการสิ่งที่เราไม่ได้ทดสอบ เพื่อให้คุณรู้ว่าควรเชื่อหน้านี้ได้แค่ไหน
Step 1 printed listener: no with EBUSY, and step 4 failed for that reason.
The cause is a setting. Our laptop's WSL runs in mirrored networking mode, which we switched
on months ago for another project. In that mode WSL itself needs to know which ports the
programs open, so WSL's own starting program, its init, asks the kernel for a seccomp listener on
the calls that give a socket a port, such as bind and listen. The programs
that it starts afterwards inherit that filter: the line filters : 1 in step 1 is that
inheritance, seen from inside our distro. You can
read the request in WSL's own source code, in the function RegisterSeccompHook
(main.cpp). The
Linux manual page for seccomp gives the rule: a second listener is refused with EBUSY.
第 1 步打印了 listener: no 和 EBUSY,第 4 步就是因为这个失败的。原因是一项设置。我们笔记本上的 WSL 运行在镜像网络(mirrored networking)模式下,
那是我们几个月前为另一个项目打开的。在这种模式下,WSL 自己需要知道程序打开了哪些端口,所以 WSL 自己的启动程序(init)会向内核要一个 seccomp 监听器,
监听给套接字分配端口的调用,例如 bind 和 listen。它之后启动的程序会继承这个过滤器:第 1 步里的 filters : 1 这一行,
就是从我们的发行版里面看到的这种继承。你可以在 WSL 自己的源代码里读到这个请求,就在
RegisterSeccompHook 函数中(main.cpp)。
Linux 关于 seccomp 的手册页给出了规则:第二个监听器会被以 EBUSY 拒绝。
ขั้นที่ 1 พิมพ์ listener: no พร้อม EBUSY และขั้นที่ 4 ล้มเหลวด้วยเหตุผลนั้น สาเหตุคือการตั้งค่าหนึ่งอย่าง WSL บนแล็ปท็อปของเราทำงานในโหมด
mirrored networking ซึ่งเราเปิดไว้เมื่อหลายเดือนก่อนสำหรับอีกโปรเจกต์หนึ่ง ในโหมดนั้น WSL เองต้องรู้ว่าโปรแกรมเปิดพอร์ตไหนบ้าง โปรแกรมเริ่มต้นของ WSL เอง คือ init จึงขอตัวรับฟัง seccomp
จากเคอร์เนลสำหรับ system call ที่กำหนดพอร์ตให้ซ็อกเก็ต เช่น bind และ listen โปรแกรมที่มันเริ่มหลังจากนั้นสืบทอดตัวกรองนี้ไป:
บรรทัด filters : 1 ในขั้นที่ 1 คือการสืบทอดนั้น เมื่อมองจากข้างใน distro ของเรา คุณอ่านคำขอนี้ได้ในซอร์สโค้ดของ WSL เอง
ในฟังก์ชัน RegisterSeccompHook (main.cpp)
หน้าคู่มือ Linux ของ seccomp ให้กฎไว้: ตัวรับฟังตัวที่สองจะถูกปฏิเสธด้วย EBUSY
Then we put it to the test. On 2026-10-10 we ran steps 1 to 4 three times in one lab: with WSL in mirrored mode, then in its usual mode, which is called NAT, then in mirrored mode again. Nothing else changed between the runs. It was the same distro, the same kernel, the same Docker and the same OpenShell, which by then was version 0.1.3. In NAT mode, step 1 printed this: 后来我们做了测试。2026-10-10 这一天,我们在同一个实验环境里把第 1 到第 4 步跑了三遍: WSL 先是镜像模式,然后是它通常的模式(叫 NAT),最后又回到镜像模式。三次运行之间别的什么都没变:同一个发行版、同一个内核、同一个 Docker、 同一个 OpenShell,这时它已经是 0.1.3 版。在 NAT 模式下,第 1 步打印的是: แล้วเราก็ทดสอบ วันที่ 2026-10-10 เรารันขั้นที่ 1 ถึง 4 สามรอบในห้องทดลองเดียวกัน: รอบแรก WSL อยู่ในโหมด mirrored รอบที่สองอยู่ในโหมดปกติของมันซึ่งเรียกว่า NAT และรอบที่สามกลับมาที่โหมด mirrored อีกครั้ง ระหว่างแต่ละรอบไม่มีอย่างอื่นเปลี่ยน distro เดิม เคอร์เนลเดิม Docker เดิม และ OpenShell เดิม ซึ่งตอนนั้นเป็นเวอร์ชัน 0.1.3 แล้ว ในโหมด NAT ขั้นที่ 1 พิมพ์ว่า:
system : Linux 6.6.114.1-microsoft-standard-WSL2
chip : x86_64
Landlock: version 3
seccomp : kill_process kill_thread trap errno user_notif trace log allow
filters : 0 already on this program before it asked
listener: yes
a container sandbox looks possible: yesAnd step 4 started the sandbox, with the Docker runtime: 而第 4 步用 Docker 运行时把沙盒启动起来了: และขั้นที่ 4 เริ่มแซนด์บ็อกซ์ได้ ด้วยรันไทม์ Docker:
$ openshell sandbox create --name first --detach
Created sandbox: first
[0.0s] Requesting compute...
[0.0s] Sandbox allocated
[0.0s] Pulling image nvcr.io/nvidia/base/ubuntu:24.04
[0.0s] Image already present
[0.4s] Starting sandbox Container created
$ openshell sandbox list
NAME CREATED PHASE
first 2026-10-09 23:33:24 Ready
$ openshell sandbox delete first
✓ Sandbox first deletion accepted; cleanup is pendingThe list gives its time in UTC, seven hours behind our clock: 23:33 there was 06:33 the next morning here. 列表里的时间是 UTC,比我们的时钟晚七个小时:那里的 23:33,是我们这里第二天早上的 06:33。 เวลาในรายการเป็น UTC ซึ่งช้ากว่านาฬิกาของเราเจ็ดชั่วโมง: 23:33 ที่นั่น คือ 06:33 ของเช้าวันถัดไปที่นี่
Back in mirrored mode, step 1 answered EBUSY again, and the sandbox ended in
Error again. So on our laptop the mode is the cause. What each of the three runs printed
is in the folder openshell_assets/checks/modes, and
README.txt says how to do it again.
回到镜像模式后,第 1 步又回答 EBUSY,沙盒又以 Error 结束。所以在我们的笔记本上,原因就是这个模式。
三次运行各自打印的内容都在 openshell_assets/checks/modes 文件夹里,README.txt 说明了怎样重做一遍。
เมื่อกลับมาที่โหมด mirrored ขั้นที่ 1 ตอบ EBUSY อีกครั้ง และแซนด์บ็อกซ์จบที่ Error อีกครั้ง ดังนั้นบนแล็ปท็อปของเรา สาเหตุคือโหมดนี้
สิ่งที่แต่ละรอบพิมพ์อยู่ในโฟลเดอร์ openshell_assets/checks/modes และ README.txt บอกวิธีทำซ้ำ
What that means for you. NAT is the mode WSL uses unless somebody changed it. So on most
Windows computers step 1 should print listener: yes and step 4 should work. If your step
1 says no, look in the file .wslconfig in your Windows user folder for the line
networkingMode=mirrored. Then you have two ways. One is the virtual-machine runtime,
which this page uses from step 5 on. The other is to change mirrored to
nat in that line and restart WSL with wsl --shutdown, which is what our
test did. Think before you take the second way: somebody switched mirrored mode on for a reason,
and other programs on your computer may need it. Ours do, so after the test we changed the line back.
这对你意味着什么。只要没有人改过,WSL 用的就是 NAT 模式。所以在大多数 Windows 电脑上,第 1 步应该打印
listener: yes,第 4 步应该能成功。如果你的第 1 步说“不能”,请在 Windows 用户文件夹里的 .wslconfig 文件中找
networkingMode=mirrored 这一行。然后有两条路。一条是虚拟机运行时,本页从第 5 步起用的就是它。另一条是把这一行里的 mirrored
改成 nat,再用 wsl --shutdown 重启 WSL,我们的测试就是这样做的。走第二条路之前先想一想:镜像模式是有人出于某个原因打开的,
你电脑上别的程序可能需要它。我们的就需要,所以测试之后我们把这一行改回去了。
สิ่งนี้หมายความว่าอย่างไรสำหรับคุณ NAT คือโหมดที่ WSL ใช้ ถ้าไม่มีใครเปลี่ยน ดังนั้นบนคอมพิวเตอร์ Windows ส่วนใหญ่
ขั้นที่ 1 ควรพิมพ์ listener: yes และขั้นที่ 4 ควรสำเร็จ ถ้าขั้นที่ 1 ของคุณตอบว่าไม่ได้ ให้ดูในไฟล์ .wslconfig ในโฟลเดอร์ผู้ใช้ Windows ของคุณ
หาบรรทัด networkingMode=mirrored จากนั้นมีสองทาง ทางแรกคือรันไทม์เครื่องเสมือน ซึ่งหน้านี้ใช้ตั้งแต่ขั้นที่ 5 เป็นต้นไป ทางที่สองคือเปลี่ยน mirrored
ในบรรทัดนั้นเป็น nat แล้วเริ่ม WSL ใหม่ด้วย wsl --shutdown ซึ่งเป็นสิ่งที่การทดสอบของเราทำ คิดก่อนเลือกทางที่สอง: มีคนเปิดโหมด mirrored ไว้ด้วยเหตุผลบางอย่าง
และโปรแกรมอื่นในคอมพิวเตอร์ของคุณอาจต้องใช้มัน ของเราต้องใช้ หลังการทดสอบเราจึงเปลี่ยนบรรทัดนั้นกลับ
Look again at the end of step 3: openshell doctor check printed
All checks passed, on the same computer where no container sandbox could start. That
check looks at Docker. It does not ask for a listener. check_kernel.py does, and that
is why this page begins with it. A check that passes tells you what was checked, and nothing more.
再看一眼第 3 步的末尾:openshell doctor check 打印了 All checks passed,而就在同一台电脑上,没有任何容器沙盒能启动。
那个检查看的是 Docker。它没有去要监听器。check_kernel.py 会去要,这也是本页从它开始的原因。
一项通过的检查只告诉你它检查了什么,仅此而已。
ดูท้ายขั้นที่ 3 อีกครั้ง: openshell doctor check พิมพ์ All checks passed บนคอมพิวเตอร์เครื่องเดียวกันกับที่แซนด์บ็อกซ์แบบคอนเทนเนอร์เริ่มไม่ได้เลย
การตรวจนั้นดูที่ Docker มันไม่ได้ขอตัวรับฟัง ส่วน check_kernel.py ขอ และนั่นคือเหตุผลที่หน้านี้เริ่มด้วยมัน
การตรวจที่ผ่านบอกคุณเพียงว่าตรวจอะไรไปบ้าง ไม่มากกว่านั้น
In WSL, your Windows files appear under /mnt/c. We first kept this page's files there,
and a Python file that we uploaded into the sandbox would not run:
source code cannot contain null bytes. The check below uploads one file twice, from the Windows drive and from the Linux
folder, into a sandbox made from OpenShell's own image. It also shows that this image has no
curl and no Python, which is why step 6 builds another one.
在 WSL 里,你的 Windows 文件出现在 /mnt/c 下面。我们一开始把本页的文件放在那里,结果上传到沙盒的一个 Python 文件无法运行:
source code cannot contain null bytes。下面的检查把同一个文件上传两次,一次来自 Windows 盘,一次来自 Linux 文件夹,
上传到一个用 OpenShell 自带镜像创建的沙盒里。它还显示了这个镜像里没有 curl,也没有 Python,这就是第 6 步要另外构建一个镜像的原因。
ใน WSL ไฟล์ Windows ของคุณปรากฏอยู่ใต้ /mnt/c ตอนแรกเราเก็บไฟล์ของหน้านี้ไว้ที่นั่น และไฟล์ Python ที่เราอัปโหลดเข้าแซนด์บ็อกซ์ก็รันไม่ได้:
source code cannot contain null bytes การตรวจด้านล่างอัปโหลดไฟล์เดียวกันสองครั้ง จากไดรฟ์ของ Windows และจากโฟลเดอร์ Linux
เข้าไปในแซนด์บ็อกซ์ที่สร้างจากอิมเมจของ OpenShell เอง มันยังแสดงด้วยว่าอิมเมจนี้ไม่มี curl และไม่มี Python ซึ่งเป็นเหตุผลที่ขั้นที่ 6 สร้างอิมเมจอีกอันหนึ่ง
# windows_drive.sh - two things to know before you build your own image.
# It makes a sandbox of its own, named plain, and removes it at the end.
# The second half only means something inside WSL on Windows.
# 1. The image a sandbox gets when you name none has neither curl nor Python.
openshell sandbox create --name plain --detach
openshell sandbox exec -n plain -- curl --version
openshell sandbox exec -n plain -- python3 --version
# 2. Upload one file twice: from the Windows drive, and from this Linux folder.
cp ask_github.py /mnt/c/Users/Public/from_windows_drive.py
openshell sandbox upload plain /mnt/c/Users/Public/from_windows_drive.py
openshell sandbox upload plain ask_github.py
# The same length. Compare the fingerprints, and look at the first bytes.
sha256sum ask_github.py
openshell sandbox exec -n plain -- sh -c 'wc -c *.py; sha256sum *.py'
openshell sandbox exec -n plain -- sh -c 'od -c from_windows_drive.py | head -2'
rm /mnt/c/Users/Public/from_windows_drive.py
openshell sandbox delete plain$ openshell sandbox create --name plain --detach
Created sandbox: plain
[0.0s] Requesting compute...
[0.0s] Sandbox allocated
[0.0s] Resolving image
[0.0s] Pulling image nvcr.io/nvidia/base/ubuntu:24.04
[0.0s] Using cached root disk
[0.0s] Image pulled (512 MB)
[0.0s] Preparing overlay disk
[0.2s] Waiting for VM supervisor
$ openshell sandbox exec -n plain -- curl --version
/bin/bash: line 1: curl: command not found
$ openshell sandbox exec -n plain -- python3 --version
/bin/bash: line 1: python3: command not found
$ cp ask_github.py /mnt/c/Users/Public/from_windows_drive.py
$ openshell sandbox upload plain /mnt/c/Users/Public/from_windows_drive.py
Uploading /mnt/c/Users/Public/from_windows_drive.py -> sandbox:~
✓ Upload complete
$ openshell sandbox upload plain ask_github.py
Uploading ask_github.py -> sandbox:~
✓ Upload complete
$ sha256sum ask_github.py
43f1e39b6775f8d2da0111925b0936adfb161778c1733c5f5a1f23ea533228fc ask_github.py
$ openshell sandbox exec -n plain -- sh -c 'wc -c *.py; sha256sum *.py'
438 ask_github.py
438 from_windows_drive.py
876 total
43f1e39b6775f8d2da0111925b0936adfb161778c1733c5f5a1f23ea533228fc ask_github.py
22581f243bd5103e1b34710841c7f6ede24d424daedc1423565fc3d800409305 from_windows_drive.py
$ openshell sandbox exec -n plain -- sh -c 'od -c from_windows_drive.py | head -2'
0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
$ rm /mnt/c/Users/Public/from_windows_drive.py
$ openshell sandbox delete plain
✓ Deleted sandbox plainRead the output. Both copies are 438 bytes long, and OpenShell
said Upload complete both times. But the fingerprints differ. The copy from the Linux
folder has the fingerprint of the file we sent. The copy from the Windows drive is all zeros:
\0 \0 \0. We do not know the cause for certain. The Windows drive tells Linux that this
small file takes no space on the disk, and a program that trusts that number would treat the file as
empty. That is our guess, and it is only a guess. The cure is simple and we tested it: keep your
files in the Linux home folder.
读输出。两份副本都是 438 字节长,而且两次 OpenShell 都说 Upload complete。但指纹不一样。
来自 Linux 文件夹的那份,指纹和我们发出的文件相同。来自 Windows 盘的那份全是零:\0 \0 \0。我们不能确定原因。Windows 盘告诉 Linux 这个小文件在磁盘上不占空间,
而一个相信这个数字的程序会把文件当成空的。这是我们的猜测,也仅仅是猜测。解决办法很简单,我们也测试过:把文件放在 Linux 的主目录里。
อ่านผลลัพธ์ สำเนาทั้งสองยาว 438 ไบต์ และ OpenShell บอกว่า Upload complete ทั้งสองครั้ง แต่ลายนิ้วมือต่างกัน
สำเนาจากโฟลเดอร์ Linux มีลายนิ้วมือของไฟล์ที่เราส่ง สำเนาจากไดรฟ์ของ Windows เป็นศูนย์ทั้งหมด: \0 \0 \0 เราไม่รู้สาเหตุแน่ชัด
ไดรฟ์ของ Windows บอก Linux ว่าไฟล์เล็กนี้ไม่ใช้พื้นที่บนดิสก์ และโปรแกรมที่เชื่อตัวเลขนั้นจะมองว่าไฟล์ว่างเปล่า นั่นคือการเดาของเรา และเป็นเพียงการเดา
วิธีแก้ง่าย และเราทดสอบแล้ว: เก็บไฟล์ของคุณไว้ในโฟลเดอร์ home ของ Linux
curl or a few lines of
Python. OpenShell's manual has a guide called
Run Your First Agent
for the next step. It needs an account with a model provider, and we did not follow it.
真正的 AI 智能体。我们沙盒里的每个程序不是 curl 就是几行 Python。OpenShell 的手册里有一篇叫
Run Your First Agent 的指南,讲的就是下一步。它需要一个模型服务商的账号,我们没有照着做。
AI agent ของจริง ทุกโปรแกรมในแซนด์บ็อกซ์ของเราเป็น curl หรือ Python ไม่กี่บรรทัด คู่มือของ OpenShell มีคำแนะนำชื่อ
Run Your First Agent สำหรับขั้นถัดไป มันต้องใช้บัญชีกับผู้ให้บริการโมเดล และเราไม่ได้ทำตามThree about the idea, three about the tool. 三条关于概念,三条关于工具。 สามข้อเกี่ยวกับแนวคิด และสามข้อเกี่ยวกับเครื่องมือ
You have put a program in a sandbox and decided what it may touch. Thank you for working through it, and see you in the next lab. 你已经把一个程序放进了沙盒,并决定了它可以碰什么。感谢你完成这一课,我们下个实验室见。 คุณได้ใส่โปรแกรมไว้ในแซนด์บ็อกซ์และตัดสินใจแล้วว่ามันแตะอะไรได้บ้าง ขอบคุณที่ทำจนจบ แล้วพบกันในห้องทดลองถัดไป
This page has no photographs, because its subject has no object to photograph: it is software. The diagrams are drawn by the page's own code, and no picture here was made by an image generator. The evidence is the terminal text in the green boxes. Every one of those boxes is a file that our computer wrote on 2026-10-09, when all the steps were run in order, once, on a WSL distro that had been created minutes before. The files, and the script that runs the whole thing again, are in the checks folder. 本页没有照片,因为它的主题没有可以拍摄的实物:它是软件。图表由页面自己的代码绘制,这里没有任何图片是由图像生成器制作的。 证据就是绿色方框里的终端文字。每一个方框都是我们的电脑在 2026-10-09 写下的文件,当时所有步骤在一个几分钟前才创建的 WSL 发行版上按顺序运行了一遍。 这些文件,以及把整件事重新运行一遍的脚本,都在 checks 文件夹里。 หน้านี้ไม่มีภาพถ่าย เพราะเรื่องที่สอนไม่มีวัตถุให้ถ่าย: มันคือซอฟต์แวร์ แผนภาพวาดโดยโค้ดของหน้านี้เอง และไม่มีภาพใดที่นี่สร้างโดยโปรแกรมสร้างภาพ หลักฐานคือข้อความเทอร์มินัลในกล่องสีเขียว ทุกกล่องคือไฟล์ที่คอมพิวเตอร์ของเราเขียนเมื่อ 2026-10-09 ตอนที่ทุกขั้นถูกรันตามลำดับหนึ่งรอบ บน WSL distro ที่เพิ่งสร้างก่อนหน้านั้นไม่กี่นาที ไฟล์เหล่านั้น และสคริปต์ที่รันทั้งหมดซ้ำอีกครั้ง อยู่ในโฟลเดอร์ checks
src/linux/init/main.cpp, function RegisterSeccompHook:
github.com/microsoft/WSL.EBUSY:
man7.org. The kernel documentation of
Landlock.docker group means:
docs.docker.com.