pVisor Job 用户场景与回归示例¶
从最简单的 Job 开始,逐步加入资源限制、stage、VM、容器和网络功能,最后走完审查、分支与轨迹回放流程。
每个 case 先说明用途、准备和预期结果,再给出可执行命令;编号便于单独回归。
run 创建 Job;status、inspect、apply、drop、fork、kill 直接操作 Job。replay 从轨迹启动 Job。编号(如 A01)只用于回归报告和问题定位。
按需求选择¶
| 你的需求 | 建议先看 |
|---|---|
| 只想运行一个命令,或确认默认写入 | A01–A03、A07 |
| 需要超时、内存或文件限制 | A04、B01–B04 |
| 想保留、丢弃或检查文件改动 | C01–C06 |
| 需要组合 OverlayFS 层或授予路径权限 | D01、D04–D06 |
| 想了解 host 默认隔离 | D02–D03 |
| 使用 VM、宿主 rootfs 或 OCI 镜像 | E01–E06 |
| 使用原生 OCI 容器 | F01–F04 |
| 配置网络代理或禁止网络 | G01–G07 |
| 接入 Gateway 或记录轨迹 | H01–H02 |
| 从配置文件或 RunSpec 执行 | I01–I03 |
| 参考多能力组合 | J01–J03 |
| 审查、选择性提交、分支或终止 Job | K01–K04 |
| 准备轨迹回放 | M01 |
| 验证终端界面与权限弹窗 | D06、M02 |
本文件同时是用户文档和 semspec 的 DOC 规格源。每个场景包含用途、语义、违反示例、
命令和断言;just cases 直接执行下方 Bash 检查。原 A01–M02 编号保留在标题中。
如何使用¶
手工执行产品命令时,先准备一个测试工作目录,并确保 pvisor 在 PATH 中。
完整检查块包含 runner 提供的夹具和断言函数,请通过 just cases 执行。
从仓库根目录运行自动检查:
just semspec list --domain DOC
just semspec run docs/src/zh/reference/cases.md --subject-bin target/release/pvisor
just cases --case S-DOC-001,S-DOC-012 --keep
just cases
just semspec show S-DOC-001
just cases 构建 release pVisor,发现本页的 54 个有效 DOC 规格和
VM 控制与 RAM backing 的六条场景,输出 JSON 报告到
target/pvisor-case-report.json。使用 S-DOC ID 选择 case;A01 对应 S-DOC-001,
C01 对应 S-DOC-012。完整映射见 tests/semantics/README.md。也可以通过
just semspec run --domain DOC --subject-bin PATH 使用已有二进制。
L01、L02 和 S-DOC-053、S-DOC-054 随 env 功能移除;这些 ID 不再复用。
每条规格把原来的命令、退出预期和全部断言放在同一个审核摘要内。预期非零退出必须
实际发生并通过原断言,不作为 xfail。断言词汇来自参与审核摘要的 cases.sh,读取当前 case
的 run-bundle.json、run.json 和命令日志。pVisor 配置、Job 数据及夹具都在
临时 CASE_ROOT;失败保留现场,--keep 保留所有现场。缺少声明的前提条件报告 SKIP。
新增规格保持 UNREVIEWED。检查成功不等于人工批准;人工完成规格、词汇和引擎审核后,
才使用 just cases --require-reviewed 作为门禁。semspec 支持的选项以 just semspec run --help
为准。
| 测试资源 | 环境变量 |
|---|---|
| Linux rootfs | PVISOR_CASE_ROOTFS;Linux 未设置时使用宿主 /,仅验证流程,不代表独立 guest rootfs |
| VM 镜像 | PVISOR_CASE_IMAGE;默认 ubuntu:latest |
| 容器镜像 | PVISOR_CASE_CONTAINER_IMAGE;默认 ubuntu:latest,与动态 pVisor ABI 兼容 |
| 自动选择的 OCI runtime | PVISOR_CASE_CONTAINER_RUNTIME;未设置时依次查找 crun/runc;F04 显式要求 runc |
| VM 内 Agent | PVISOR_CASE_AGENT;需在 guest 中可执行 |
Linux host stage 需要 user/mount namespace 和 FUSE,macOS stage 需要可用 macFUSE。
VM 示例需要 Linux 和 /dev/kvm。OCI runtime 可执行文件存在不保证具备运行容器的权限。
这些限制仍会由实际运行和断言检验,不自动当作测试通过。
可执行 Case¶
A. 基础调用与身份¶
这一组适合第一次使用 pVisor。先从 A01 开始;只有需要固定显示名、采集输出或显式传递环境变量时,再选择后续例子。
S-DOC-001:A01 省略 run 的最简调用¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
语义:输出当前 workspace 的绝对路径并成功退出。默认使用 host executor,不启用 stage;未配置网络策略时记录为 ambient。
理由:在当前目录执行一个命令,不需要显式写出 run。-- 后全部是交给 Agent 的命令和参数。
违反示例:命令退出 0,但输出的是父目录,或默认启动了 stage。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor -- /bin/pwd
CASE_COMMAND
stdout_has "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect run.state completed
bundle_expect run.exit_code 0
bundle_expect run.agent pwd
bundle_expect network.policy.mode ambient
S-DOC-002:A02 显式 run 与省略形式等价¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
语义:两个输出文件内容相同,都是当前工作目录。两次运行会各自生成记录,Job ID 和时间可以不同。
理由:对比省略和显式写出 run 的两种调用。分别保存 Agent 的标准输出,便于比较。
违反示例:显式 run 与省略形式的工作目录输出不同。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor -- /bin/pwd > implicit.txt
pvisor run -- /bin/pwd > explicit.txt
CASE_COMMAND
diff implicit.txt explicit.txt
test "$(cat implicit.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect run.agent pwd
S-DOC-003:A03 Job 名称和 stdio capture¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
语义:Job 名称为 smoke,结果中的标准输出为 hello,未被截断。
理由:为这次运行命名,并将 Agent 输出保存到运行结果。--name smoke 指定显示名,--stdio capture 开启输出采集。
违反示例:Agent 名称丢失,或捕获的 hello 被标记为截断。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --name smoke --stdio capture -- /bin/sh -c 'printf hello'
CASE_COMMAND
bundle_expect run.agent smoke
bundle_expect run.output.stdout hello
bundle_expect run.output.stdout_truncated false
S-DOC-004:A04 超时¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
语义:pVisor 非零退出,运行结果的失败类型为 deadline_exceeded。
理由:给运行设置墙钟超时。100ms 是从运行开始计时的持续时间,不是 CPU 时间;命令故意睡眠 10 秒。
违反示例:超时命令仍成功退出,或失败被记成可重试的普通 process_exit。
require_python3
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --timeout 100ms -- /bin/sleep 10
CASE_COMMAND
bundle_expect run.state failed
bundle_expect run.failure.kind deadline_exceeded
bundle_expect run.failure.retryable false
S-DOC-005:A05 严格执行模式拒绝 best-effort 边界¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:当前 host / container / VM 执行路径在启动 Agent 前均因缺少 Subprocess
enforcement 证据而拒绝请求(UnsupportedPolicy)。此例验证 fail-closed,
不代表 --strict 当前在任一 executor 上可达“更强沙箱已就绪”。
理由:要求严格执行能力检查。--strict 不接受所请求能力缺少强制执行证据;这里同时要求禁止网络。
违反示例:缺少请求能力的强制证据时仍启动 Agent,或拒绝时没有说明缺失的能力。
require_python3
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --strict --overlaynet-deny-all -- "$CASE_TRUE"
CASE_COMMAND
stdout_has "lacks enforced evidence for requested capability dimensions"
S-DOC-006:A06 显式环境投影¶
建议场景:适合第一次使用 pVisor、确认命令和 Job 身份。
语义:子进程可见 TEST_PVISOR_VALUE=visible;运行记录列出这个变量,但不声明整体继承宿主环境。
理由:只把指定的宿主环境变量传给子进程。变量仅为这条命令设置,通过 --pass-env 显式允许投影。
违反示例:显式允许的变量未传给 Agent,或记录错误声明继承了整个宿主环境。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
TEST_PVISOR_VALUE=visible pvisor --pass-env TEST_PVISOR_VALUE -- /usr/bin/env
CASE_COMMAND
stdout_has "TEST_PVISOR_VALUE=visible"
bundle_contains environment.projected_keys TEST_PVISOR_VALUE
bundle_expect environment.inherits_host false
S-DOC-007:A07 默认写入直接到 workspace¶
语义:命令退出后,direct.txt 直接出现在原 workspace,记录中没有 OverlayFS stage。
理由:验证普通 host Job 的默认可写 lower;无需为日常命令额外选择执行器或 stage。
违反示例:没有请求 stage 的 host 写入没有直接出现在工作区。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor -- /bin/sh -c 'printf direct > direct.txt'
CASE_COMMAND
test "$(cat direct.txt)" = direct
record_expect overlay null
B. 资源限制¶
这一组展示“请求限制”和“实际强制”之间的区别。B01 用于查看完整配置,B02 才真正尝试触发文件大小限制。
S-DOC-008:B01 组合使用所有资源限制¶
建议场景:适合需要控制或验证资源限制的任务。
语义:命令成功退出,五个请求值出现在运行记录中,同时报告生效值和限制机制。/bin/true 不消耗这些额度,此例不测试超限行为。
理由:组合设置内存、进程数、CPU 时间、打开文件数和单文件大小。MiB 是二进制单位;--max-cpu-time 与墙钟超时不同。
违反示例:限制参数被接受,但请求值记录错误,或缺少文件大小的生效值和 rlimit 机制。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor \
--memory 256MiB \
--max-processes 32 \
--max-cpu-time 5s \
--max-open-files 128 \
--max-file-size 1MiB \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect resources.requested.memory_bytes 268435456
bundle_expect resources.requested.processes 32
bundle_expect resources.requested.cpu_time_ms 5000
bundle_expect resources.requested.open_files 128
bundle_expect resources.requested.file_size_bytes 1048576
bundle_expect resources.effective.file_size_bytes 1048576
bundle_contains resources.mechanisms rlimit
S-DOC-009:B02 文件大小限制实际生效¶
建议场景:适合需要控制或验证资源限制的任务。
语义:写入命令失败,落盘文件如果存在,其大小不超过 1024 字节;运行结果记录进程退出失败。
理由:验证单文件大小限制:将上限设为 1KiB,再尝试用 dd 写入 4KiB。
违反示例:dd 成功写出 4KiB,或失败后文件仍超过 1024 字节。
require_python3
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --max-file-size 1KiB -- /bin/sh -c 'dd if=/dev/zero of=large bs=4096 count=1'
CASE_COMMAND
bundle_expect resources.requested.file_size_bytes 1024
bundle_expect run.state failed
bundle_expect run.failure.kind process_exit
test ! -f large || [ "$(wc -c < large)" -le 1024 ]
S-DOC-010:B03 内存参数短别名¶
建议场景:适合需要控制或验证资源限制的任务。
语义:命令成功,记录中的请求值为 268435456 字节,与 --memory 256MiB 一致。
理由:使用 --memory 的别名 --mem,为一个简单命令设置 256MiB 内存额度。
违反示例:--mem 256MiB 被解析成不同于 --memory 的请求值。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --mem 256MiB -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect resources.requested.memory_bytes 268435456
S-DOC-011:B04 Stage 总大小限制¶
建议场景:适合需要控制或验证资源限制的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:stage 成功建立并保存在指定路径。此例只验证参数可用和目录建立;当前产物未记录该上限,也未在此例中尝试写满 stage。
理由:为持久 stage 请求 1GiB 的总大小限制。它限制的是 stage 总量,和 B02 的单个文件大小不是同一个概念。
违反示例:stage 命令成功退出,但文件系统状态未标为 staged,或存储路径不符。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/limited-stage" --overlayfs-max-size 1GiB -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect filesystem.state staged
bundle_expect safety.filesystem_changes_staged true
record_expect storage "$(realpath "$PVISOR_CASE_ROOT/limited-stage")"
C. Stage 与 whole-rootfs¶
当你希望 Agent 可以自由修改文件、但不污染当前 workspace 时使用这一组。C01 是最常用的持久模式;C02 使用 --safe 自动选择并保留 stage,C03 演示对持久 stage 显式执行 drop。
S-DOC-012:C01 持久 stage¶
建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:原 workspace 没有 result.txt;变更清单中出现该文件,指定 stage 内保留 run-bundle.json,便于之后查看。
理由:把本次运行的文件改动放进一个保留的 stage。命令在 workspace 里创建 result.txt。
违反示例:result.txt 穿透到原工作区,或变更清单遗漏它。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/stage-keep" -- /bin/sh -c 'printf changed > result.txt'
CASE_COMMAND
bundle_expect filesystem.state staged
bundle_contains filesystem.changes result.txt
bundle_expect safety.filesystem_write_non_bypassable true
test ! -e result.txt
test -f "$PVISOR_CASE_ROOT/stage-keep/run-bundle.json"
S-DOC-013:C02 默认保留 stage¶
建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:命令成功,日志中给出的存储目录及 Run Bundle 保留,原 workspace 没有新建的文件。
理由:无需手写存储路径。--safe 在没有指定 --stage 时使用持久 Job 存储,退出后保留改动。
违反示例:日志中的 Run Bundle 路径在退出后消失,或 result.txt 出现在原工作区。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor --safe -- /bin/sh -c 'printf changed > result.txt'
CASE_COMMAND
storage=$(dirname "$(grep -m1 '^Run Bundle: ' "$PVISOR_CASE_STDOUT" | cut -d' ' -f3-)")
test -n "$storage"
test -f "$storage/run-bundle.json"
test ! -e result.txt
S-DOC-014:C03 显式丢弃持久 stage 的改动¶
建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:stage 目录保留,运行记录的文件系统状态变为 discarded;原 workspace 没有新文件。
理由:指定持久 stage 路径,完成运行后通过 pvisor drop 显式丢弃其中的改动。
违反示例:drop 后记录仍标为 staged,或原工作区得到 result.txt。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/stage-drop" -- /bin/sh -c 'printf changed > result.txt'
pvisor drop "$CASE_ROOT/stage-drop"
CASE_COMMAND
record_expect overlay.state discarded "$PVISOR_CASE_ROOT/stage-drop"
test ! -e result.txt
test -f "$PVISOR_CASE_ROOT/stage-drop/run-bundle.json"
S-DOC-015:C04 显式 stage 保留已有目录内容¶
建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。
语义:命令成功,原有的 user-file 和新生成的 Run Bundle 都保存在指定目录。
理由:验证 --stage PATH 始终表示持久目录;目录里已有的用户文件不会因运行结束而被删除。
违反示例:运行清理删除了指定 stage 目录中原有的 user-file。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
mkdir -p "$CASE_ROOT/existing-stage"
touch "$CASE_ROOT/existing-stage/user-file"
pvisor --stage "$CASE_ROOT/existing-stage" -- "$CASE_TRUE"
CASE_COMMAND
test -f "$PVISOR_CASE_ROOT/existing-stage/user-file"
test -f "$PVISOR_CASE_ROOT/existing-stage/run-bundle.json"
S-DOC-016:C05 whole-rootfs 捕获与 tmpfs 隔离¶
建议场景:适合隔离文件变更、保留 stage 或验证 whole-rootfs 的任务。
准备:Linux user/mount namespace 可用;macOS Seatbelt 不提供此例要求的 whole-rootfs/tmpfs 隔离。
语义:workspace 的改动出现在 stage,宿主 workspace 和宿主 /tmp 均不出现新文件。这里不验证 workspace 以外普通 rootfs 路径的持久化。
理由:比较 workspace 写入和 sandbox 临时目录写入。前者用于保留任务改动,后者只供本次运行临时使用。
违反示例:工作区写入未被暂存,或 sandbox 的 /tmp 写入出现在宿主 /tmp。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/root-stage" -- /bin/sh -c \
'printf workspace > ./workspace-change; printf tmp > "$1"' sh "$CASE_TMP_PATH"
CASE_COMMAND
bundle_contains filesystem.changes workspace-change
test ! -e workspace-change
test ! -e "$CASE_TMP_PATH"
S-DOC-017:C06 --safe 隔离 HOME 写入¶
准备:Linux user/mount namespace 可用。
语义:Agent 能在自己的 HOME 中读回刚写入的状态;宿主 HOME 没有该文件,workspace 的持久 stage 仍可审查。
理由:检查 --safe 除暂存 workspace 外,还为 HOME 提供独立的写时复制视图。示例把测试 HOME 放在专用目录,不触碰真实用户目录。
违反示例:--safe 把 HOME/state 写到了宿主 HOME,或 Agent 不能读取自己的写入。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
mkdir -p "$CASE_ROOT/home"
HOME="$CASE_ROOT/home" pvisor --safe --stage "$CASE_ROOT/safe-home" -- \
/bin/sh -c 'printf private > "$HOME/state"; cat "$HOME/state"'
CASE_COMMAND
stdout_has private
test ! -e "$PVISOR_CASE_ROOT/home/state"
bundle_expect filesystem.state staged "$PVISOR_CASE_ROOT/safe-home"
bundle_expect network.policy.mode allowlist "$PVISOR_CASE_ROOT/safe-home"
D. OverlayFS 与 Host 安全边界¶
D01 讲视图层组合,D02/D03 讲 host executor,D04–D06 讲拒绝、显式写入和交互授权。
文件规则为 deny、ask、warn:分别表示拒绝、询问、放行并警告;默认累加。
--mount 的 read 授予只读宿主共享(要求 host executor 加 --safe/--ask),
stage 组合写时复制底层,write 直接写入宿主 lower。
--access PATH-GLOB:ask 会自动启用审计 TUI 和 safe 暂存视图,无需另加 --ask。
文件弹窗的 1 仅允许此文件,2 允许同级目录中的文件,3 允许相同后缀的文件;
d 拒绝此目标。明确的 deny 规则仍直接拒绝,不弹窗。
弹窗默认仅对当前 session 生效;先按 s、w、u,分别选择 session、workspace、user,
再按数字选择授权范围,按 Enter 确认;默认选中拒绝按钮,d 直接拒绝。session 规则写入当前 Job 的 audit-policy.json;
workspace 和 user 规则写入 ~/.config/pvisor/config.toml 的 permissions 部分
(设置了绝对路径 XDG_CONFIG_HOME 时使用该目录)。workspace 按规范化后的工作目录区分。
后续 TUI Job 加载这些规则,优先级为 session > workspace > user,同层最后匹配的规则生效。
持久化文件路径使用原始绝对路径;用户级后缀规则可覆盖其他工作区,授权时应注意范围。
可在 Permissions 面板查看规则,用 j/k 选择后按两次 x 移除决定;随后可能命中其他规则或重新询问。
audit.jsonl 记录人工及自动决策和保存范围。Job 记录默认保留;--stage PATH 可指定位置。
S-DOC-018:D01 高级 OverlayFS 组合¶
建议场景:适合检查 OverlayFS 视图和 host 安全边界。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:记录的目标为 view,从顶层到底层依次为 layer、base、本次运行持有的 workspace 快照。目录为空,因此此例检查配置顺序,不检查同名文件覆盖内容。
理由:把宿主的两个目录依次叠加到工作区视图,并指定 Agent 看到的路径。directory 选择目录后端;改动只通过显式 apply 提交。
违反示例:记录中 lower 的 layer/base 顺序颠倒,或 workspace 快照目录缺失。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
mkdir -p "$CASE_ROOT/base" "$CASE_ROOT/layer" "$PWD/view"
pvisor \
--stage "$CASE_ROOT/composed-stage" \
--mount "$CASE_ROOT/base:$PWD/view:stage" \
--mount "$CASE_ROOT/layer:$PWD/view:stage" \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect filesystem.state staged
record_expect overlay_lowers.0 "$(realpath "$PVISOR_CASE_ROOT/layer")"
record_expect overlay_lowers.1 "$(realpath "$PVISOR_CASE_ROOT/base")"
record_contains overlay_lowers.2 "$PVISOR_CASE_ROOT/composed-stage/.overlay-lowers/"
test -d "$(record_get overlay_lowers.2)"
S-DOC-019:D02 显式 host executor¶
建议场景:适合检查 OverlayFS 视图和 host 安全边界。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:Linux 记录为 rootless_process,macOS 记录为 sandboxed_process;两者都不应降级为 host process。
理由:显式选择 host executor,观察当前系统上的隔离类型。
违反示例:显式 host 请求降级为 host_process,而记录仍被当作隔离成功。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --executor host -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.kind process
if [ "$(uname -s)" = "Darwin" ]; then
bundle_expect run.executor.isolation sandboxed_process
else
bundle_expect run.executor.isolation rootless_process
fi
bundle_expect safety.host_process false
S-DOC-020:D03 host stage 隐藏原 workspace¶
建议场景:适合检查 OverlayFS 视图和 host 安全边界。
准备:Linux user/mount namespace 可用;macOS 不支持此例的 procfs/mount namespace 路径隐藏语义。
语义:cwd 指向 stage 的 merged 目录,输出中不出现原 workspace 路径。此例只检查路径显示,不证明所有原路径或继承 FD 访问都已被禁止。
理由:观察启用 stage 后子进程的 cwd 和 procfs 路径。三条命令的输出保存到 views.txt。
违反示例:启用 host stage 后 procfs 的 cwd 仍暴露原工作区路径。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/host-stage" -- /bin/sh -c \
'pwd; readlink /proc/self/root; readlink /proc/self/cwd' > views.txt
CASE_COMMAND
merged="$PVISOR_CASE_ROOT/host-stage/merged"
test "$(sed -n 1p views.txt)" = "$merged"
test "$(sed -n 3p views.txt)" = "$merged"
! grep -Fq -- "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)" views.txt
S-DOC-021:D04 显式拒绝敏感路径读取¶
准备:Linux user/mount namespace 可用。
语义:读取失败并记录拒绝规则;宿主文件保持原样。
理由:用 --access PATH-GLOB:deny 阻止 Agent 在工作区中读取匹配的文件。
违反示例:private/token 被允许读取,或拒绝后审查记录没有该目标。
require_python3
require_rootless
case_setup
case_run nonzero <<'CASE_COMMAND'
mkdir -p private
printf secret > private/token
pvisor --stage "$CASE_ROOT/access-stage" --access 'private/**:deny' -- \
/bin/cat private/token
CASE_COMMAND
stdout_has 'pVisor file access denied'
bundle_expect filesystem.access_policy.deny.0 'private/**'
bundle_contains run_observation.filesystem.paths private/token
test "$(cat private/token)" = secret
S-DOC-022:D05 显式共享路径的直接写入¶
准备:Linux user/mount namespace 可用。
语义:共享目录的 out 直接写入宿主 lower;无需对它执行 pvisor apply。
理由:用 --mount SOURCE:write 授予一个工作区之外的宿主目录可写访问。
违反示例:明确授予 write 的共享路径没有获得 mounted 内容。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
mkdir -p "$CASE_ROOT/shared"
pvisor --mount "$CASE_ROOT/shared":write -- \
/bin/sh -c 'printf mounted > "$1"' sh "$CASE_ROOT/shared/out"
CASE_COMMAND
test "$(cat "$PVISOR_CASE_ROOT/shared/out")" = mounted
S-DOC-023:D06 ask 弹窗与当前 Job 的目录授权¶
准备:Linux user/mount namespace 和 Python 3 可用。示例用伪终端自动输入 2、Enter;手工运行时在弹窗中选择后按 Enter 确认。
语义:只出现一次文件授权弹窗,两个文件均可读取;audit-policy.json 保存目录规则,audit.jsonl 记录第二次自动允许。--stage 保留当前 Job 的审计记录,不会把选择变成全局配置。
理由:用 --access 'private/*.txt:ask' 启动审计 TUI;第一次读取时按 2、Enter 授权同级目录,再读取另一文件,验证规则自动复用。
违反示例:第二个同级文件再次弹窗,或目录授权被持久化为错误范围。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
mkdir -p private
printf ASK_ONE > private/one.txt
printf ASK_TWO > private/two.txt
python3 - <<'PY'
import fcntl, os, pty, select, signal, struct, time
pid, master = pty.fork()
if pid == 0:
os.environ['TERM'] = 'xterm-256color'
os.execvp('pvisor', [
'pvisor', '--no-agent-defaults', '--stage', os.environ['CASE_ROOT'] + '/ask-stage',
'--access', 'private/*.txt:ask', '--', '/bin/sh', '-c',
'cat private/one.txt; sleep 1; cat private/two.txt',
])
fcntl.ioctl(master, 0x5414, struct.pack('HHHH', 24, 100, 0, 0))
screen = bytearray()
prompted = False
status = None
deadline = time.monotonic() + 25
try:
while time.monotonic() < deadline:
ready, _, _ = select.select([master], [], [], 0.1)
if ready:
try:
screen.extend(os.read(master, 65536))
except OSError:
pass
if not prompted and b'FILE ACCESS PAUSED' in screen:
os.write(master, b'2\r')
prompted = True
ended, result = os.waitpid(pid, os.WNOHANG)
if ended:
status = result
break
if status is None:
os.killpg(pid, signal.SIGTERM)
_, status = os.waitpid(pid, 0)
raise RuntimeError('timed out waiting for the Job')
finally:
os.close(master)
assert prompted and os.waitstatus_to_exitcode(status) == 0
assert b'ASK_ONE' in screen and b'ASK_TWO' in screen
print('ASK directory grant reused')
PY
CASE_COMMAND
stdout_has 'ASK directory grant reused'
python3 - <<'PY'
import json, os
from pathlib import Path
stage = Path(os.environ['CASE_ROOT'] + '/ask-stage')
policy = json.loads((stage / 'audit-policy.json').read_text())
assert any(rule['kind'] == 'file' and rule['scope'] == 'directory'
and rule['value'] == 'private' and rule['decision'] == 'allow'
for rule in policy['rules'])
decisions = [json.loads(line) for line in (stage / 'audit.jsonl').read_text().splitlines()]
assert any(item['request']['target'] == 'private/two.txt'
and item['decision'] == 'allow' and item['automatic']
for item in decisions)
PY
test "$(cat private/one.txt)" = ASK_ONE
test "$(cat private/two.txt)" = ASK_TWO
E. VM 与 rootfs¶
需要更强边界、独立 guest kernel 或 OCI rootfs 时使用 VM。E01 最接近“直接运行”,E02/E03 展示目录和镜像来源,E04/E05 再加入资源与 stage。
S-DOC-024:E01 --vm 简写与 host rootfs¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
准备:Linux;可访问 /dev/kvm。
语义:guest 输出与宿主 workspace 相同的绝对路径。运行结果标记为虚拟机,网络使用 pVisor 的 smoltcp 驱动。
理由:用 --vm 选择 VM executor;Linux 默认以宿主根目录作为 guest rootfs。该方式扩大了 guest 可读取的宿主文件范围,只应在可信测试环境使用。
违反示例:VM 成功退出但 guest cwd 不同,或网络没有记录 vm-smoltcp 边界。
require_python3
require_linux
require_kvm
case_setup
case_run success <<'CASE_COMMAND'
pvisor --vm -- /bin/pwd > guest-cwd.txt
CASE_COMMAND
test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect run.executor.kind virtual_machine
bundle_expect run.executor.isolation virtual_machine
bundle_expect network.interception.driver vm-smoltcp
bundle_expect network.interception.strength non-bypassable
bundle_expect safety.network_non_bypassable true
S-DOC-025:E02 显式 VM executor 与目录 rootfs¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS。
语义:虚拟机成功执行命令,guest cwd 与宿主 workspace 路径一致。
理由:已有 Linux rootfs 时,直接把目录交给 VM 使用。目录内需要有可执行的 /bin/pwd 及其运行依赖。
违反示例:目录 rootfs 的 VM 返回宿主工作区之外的 cwd。
require_python3
require_linux
require_kvm
require_rootfs
case_setup
case_run success <<'CASE_COMMAND'
pvisor --executor vm --rootfs "$CASE_ROOTFS" -- /bin/pwd > guest-cwd.txt
CASE_COMMAND
test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect run.executor.isolation virtual_machine
S-DOC-026:E03 image rootfs¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE。
语义:镜像准备后启动 VM,guest 的工作目录与宿主 workspace 路径一致。
理由:使用 OCI 镜像准备 VM 的 rootfs,不依赖 Docker/Podman daemon。将 image= 后的占位符替换为可获取的镜像引用。
违反示例:镜像 VM 返回错误 cwd,或运行结果不标为 virtual_machine。
require_python3
require_linux
require_kvm
require_image
case_setup
case_run success <<'CASE_COMMAND'
pvisor --vm --rootfs "image=$CASE_IMAGE" -- /bin/pwd > guest-cwd.txt
CASE_COMMAND
test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect run.executor.isolation virtual_machine
S-DOC-027:E04 VM 资源配置¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS。
语义:VM 成功运行,内存请求记录为 2147483648 字节。CPU 数量未由本例断言核验。
理由:使用已有 rootfs 启动 VM,同时指定 2GiB 内存和 2 个虚拟 CPU。Linux 静态构建已内嵌内核。
违反示例:2GiB 的内存请求在产物中被记成其他值。
require_python3
require_linux
require_kvm
require_rootfs
case_setup
case_run success <<'CASE_COMMAND'
pvisor --vm \
--rootfs "$CASE_ROOTFS" \
--memory 2GiB \
--cpu 2 \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.isolation virtual_machine
bundle_expect resources.requested.memory_bytes 2147483648
S-DOC-028:E05 VM workspace 与 whole-rootfs stage 组合¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE。
语义:guest cwd 保持一致,stage 保留在 vm-stage。本例只执行 pwd,验证路径和 stage 建立,不验证写入捕获。
理由:在 VM 镜像运行基础上增加持久 stage。工作区路径默认保持与宿主 cwd 一致。
违反示例:VM stage 路径或 guest cwd 与请求不一致。
require_python3
require_linux
require_kvm
require_image
case_setup
case_run success <<'CASE_COMMAND'
pvisor --vm \
--rootfs "image=$CASE_IMAGE" \
--stage "$CASE_ROOT/vm-stage" \
-- /bin/pwd > guest-cwd.txt
CASE_COMMAND
test "$(cat guest-cwd.txt)" = "$(cd "$PVISOR_CASE_WORKSPACE" && pwd -P)"
bundle_expect filesystem.state staged
record_expect storage "$PVISOR_CASE_ROOT/vm-stage"
S-DOC-029:E06 拒绝 executor 冲突¶
建议场景:适合需要 VM guest kernel、独立 rootfs 或更强隔离的任务。
语义:参数归一化阶段失败,错误信息明确指出 --vm 与非 VM executor 冲突。
理由:验证互相冲突的 executor 参数不能一起使用:--vm 选择虚拟机,--executor host 却选择宿主。
违反示例:--vm 与 --executor host 的冲突被忽略并启动了工作负载。
require_python3
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --vm --executor host --rootfs host -- "$CASE_TRUE"
CASE_COMMAND
stdout_has "--vm cannot be combined with a non-vm --executor"
F. Container¶
需要复用 OCI rootfs、但不想运行 Docker/Podman daemon 时使用原生 OCI container。请按 F01 → F04 逐步增加复杂度;F04 组合高级配置。
这些例子使用原生 OCI bundle,由 pVisor 准备文件系统并调用 runc/crun, 不依赖 Docker/Podman daemon。F01–F03 使用默认 runtime,F04 显式选择 runc。 镜像或目录需与本机架构兼容;如果当前 pVisor 是动态链接构建,guest 必须提供 相应的动态加载器和库,否则应像 F04 一样指定兼容的静态构建。 默认注入当前 pVisor,不需要在最小命令中显式指定 binary。
F04 显式设置 --container-platform linux/amd64,假设 Linux x86_64。此选项接受匹配的原生架构断言,拒绝跨架构值,即使提供了预制 rootfs;host/VM 配置会拒绝它。它不是模拟执行、程序自动发现或下载选择器。现有用例检查组合启动,不是平台拒绝矩阵。见容器准备与配置。
S-DOC-030:F01 最小 container Job¶
建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。
准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。
语义:pVisor 准备 OCI bundle、注入自身并执行 /bin/true,运行结果记录为 container 且退出码为 0。
理由:以 OCI 镜像启动最小容器运行。--container-image 同时选择容器 executor 和镜像来源。
违反示例:最小容器命令退出非零,或产物把 container 记为其他 executor。
require_python3
require_container
case_setup
case_run success <<'CASE_COMMAND'
pvisor --container-runtime "$CASE_CONTAINER_RUNTIME" --container-image "$CASE_CONTAINER_IMAGE" -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.kind container
bundle_expect run.state completed
bundle_expect run.exit_code 0
S-DOC-031:F02 container rootfs 与隔离网络¶
建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。
准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。
语义:容器中的 /bin/true 成功退出。此命令不发起网络请求,因此断言只检查启动成功,不验证网络是否能被绕过。
理由:在 F01 基础上只增加 --container-network none,让容器使用独立的网络 namespace,不配置外部连接。
违反示例:隔离网络配置使简单容器命令无法正常启动或完成。
require_python3
require_container
case_setup
case_run success <<'CASE_COMMAND'
pvisor --container-runtime "$CASE_CONTAINER_RUNTIME" --container-image "$CASE_CONTAINER_IMAGE" \
--container-network none \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.kind container
bundle_expect run.state completed
S-DOC-032:F03 使用宿主 rootfs 的 OCI bundle¶
建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。
准备:Linux、可运行的 OCI runtime,以及允许 rootless container 的 user namespace。
语义:以指定目录作为容器根文件系统执行命令,不需要配置镜像仓库。使用专用测试 rootfs,不要把宿主 / 当作此例的测试目录。
理由:不提供容器镜像,直接以宿主 / 作为只读 lower。pVisor 会先建立独立 synthetic rootfs,再把宿主标准目录以只读方式映射进去。
违反示例:宿主 rootfs 的 OCI bundle 没有作为 container 正常完成。
require_python3
require_container_runtime
require_linux
case_setup
case_run success <<'CASE_COMMAND'
pvisor --container-runtime "$CASE_CONTAINER_RUNTIME" --executor container \
--rootfs host \
--container-network none \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.kind container
bundle_expect run.state completed
S-DOC-033:F04 显式 OCI runtime 与高级 container 参数¶
建议场景:适合由 runc/crun 直接启动 OCI 容器的任务。
准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。
语义:容器成功退出。read_only=false 使绑定目录可写,即使 rootfs 只读;--container-workdir 在 Run 没有 cwd 时才作为回退。此例仅检查组合启动,未分别检查用户身份和读写行为。
理由:显式覆盖高级容器选项:使用 runc 和指定 pVisor 构建,声明平台、uid/gid、工作目录、只读 rootfs,并把宿主目录绑定到 /workspace。
违反示例:高级 OCI 参数被接受但容器失败,或被错误记为其他 executor。
require_python3
require_container
require_runc
case_setup
case_run success <<'CASE_COMMAND'
pvisor --executor container \
--container-runtime runc \
--rootfs "image=$CASE_CONTAINER_IMAGE" \
--container-pvisor-binary "$SUBJECT_BIN" \
--container-platform linux/amd64 \
--container-network none \
--container-workdir /workspace \
--container-user 1000:1000 \
--container-read-only-rootfs \
--container-mount "source=\"$WS\",target=\"/workspace\",read_only=false" \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.executor.kind container
bundle_expect run.state completed
G. OverlayNet¶
这一组只讨论网络边界。proxy 适合需要 host Gateway 的协作式访问,VM auto 和 host deny-all 才适合需要更强网络边界的场景。
使用 --ask 时,未列入规则的代理网络目标会暂停并弹窗:1 仅允许当前目标,
2 允许当前主机名及其子域名,范围仍限于相同端口和传输协议;IP 地址没有域名选项,
d 拒绝当前目标。与文件授权一样,先按 s / w / u 选择 session / workspace / user 保存范围。
显式拒绝规则不进入弹窗;未经代理的直接 socket 连接也不会触发此审计。
S-DOC-034:G01 启用默认 proxy¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
语义:记录为 explicit-proxy,拦截强度为 cooperative。它只约束经过代理的流量,不能据此认为直接 socket 已被禁止。
理由:只给出 --overlaynet,省略值时启用默认 proxy 模式。
违反示例:默认 proxy 请求没有记录 explicit-proxy/cooperative,或 capture 产物缺失。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --overlaynet -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect network.interception.driver explicit-proxy
bundle_expect network.interception.strength cooperative
bundle_contains artifacts capture
S-DOC-035:G02 自定义 proxy 监听地址¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
语义:记录的 OverlayNet 监听地址与请求一致,网络驱动为 explicit-proxy。
理由:指定代理监听地址;该参数会自动启用 host proxy。手工运行时确保 18080 端口未被占用;脚本会替换为空闲端口。
违反示例:实际监听地址与所请求的 loopback 地址不一致。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --overlaynet-listen "$CASE_PROXY_LISTEN" -- "$CASE_TRUE"
CASE_COMMAND
record_expect overlaynet_listen "$CASE_PROXY_LISTEN"
bundle_expect network.interception.driver explicit-proxy
S-DOC-036:G03 allow、deny 和带宽限制组合¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
语义:记录为 allowlist 模式,允许 api.example.com:443,拒绝 10.0.0.0/8,并把 1mbps 记录为每秒 125000 字节。本例不实际发请求。
理由:组合配置允许目标、拒绝网段和针对目标的带宽上限。只有通过代理的流量才受这些规则约束。
违反示例:allow、deny 或带宽规则中的主机、端口、速率被丢失或改写。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --overlaynet-allow api.example.com:443 \
--overlaynet-deny 10.0.0.0/8 \
--overlaynet-limit api.example.com=1mbps \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect network.policy.mode allowlist
bundle_expect network.policy.rules.0.host api.example.com
bundle_expect network.policy.rules.0.ports.0 443
bundle_expect network.policy.deny_rules.0.host 10.0.0.0/8
bundle_expect network.policy.limits.0.host api.example.com
bundle_expect network.policy.limits.0.bytes_per_second 125000
S-DOC-037:G04 deny-all 不可通过环境变量绕过¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
准备:安装 curl。
语义:命令失败,结果记录 no-network 和不可绕过边界。外网自身不可用也会使 curl 失败,因此本例不能单独证明隔离有效。
理由:验证禁止网络后,清除常见代理变量仍不能访问外网。Linux 使用 network namespace,macOS 使用 Seatbelt;需要宿主安装 curl,命令故意发起网络请求。
违反示例:清除代理环境变量后 curl 成功连到外网,或安全产物未声明强制边界。
require_python3
require_curl
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --overlaynet-deny-all -- /bin/sh -c \
'unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy; curl --max-time 2 https://example.com'
CASE_COMMAND
bundle_expect network.policy.mode no-network
bundle_expect safety.network_non_bypassable true
bundle_expect run.state failed
S-DOC-038:G05 VM OverlayNet auto¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
准备:Linux;可访问 /dev/kvm;准备好 Linux rootfs,并为脚本设置 PVISOR_CASE_ROOTFS。
语义:记录为 vm-smoltcp 和 non-bypassable,而不是 host 的协作式代理。
理由:验证 VM 默认使用 OverlayNet auto,使流量经过虚拟机的 smoltcp 网络驱动。
违反示例:VM 网络被记录为 cooperative proxy 而非 vm-smoltcp 强制边界。
require_python3
require_linux
require_kvm
require_rootfs
case_setup
case_run success <<'CASE_COMMAND'
pvisor --vm --rootfs "$CASE_ROOTFS" -- "$CASE_TRUE"
CASE_COMMAND
bundle_expect network.interception.driver vm-smoltcp
bundle_expect network.interception.strength non-bypassable
S-DOC-039:G06 关闭 OverlayNet 时拒绝策略参数¶
建议场景:适合配置出站网络、代理访问或禁止网络的任务。
语义:启动前失败,错误提示策略需要 auto 或 proxy。如果只想关闭 OverlayNet,请不要附带 allow/deny/limit。
理由:检查关闭 OverlayNet 后不能继续提供网络策略。
违反示例:关闭 OverlayNet 后仍静默接受 allow 策略。
require_python3
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --overlaynet off --overlaynet-allow example.com:443 -- "$CASE_TRUE"
CASE_COMMAND
stdout_has "OverlayNet policy options require --overlaynet auto or proxy"
S-DOC-040:G07 审查被代理拒绝的具体目标¶
准备:安装 curl。
语义:curl 请求失败;status --review 的 Network access observations 可指出 blocked.example:80 被拒绝。host proxy 是协作式边界,本例只证明经过代理的请求被拦截。
理由:通过 pVisor 注入的代理访问一个明确拒绝的域名,确认 Job 记录的是具体目标和拒绝次数,而不只是网络失败总数。请求在策略层被拒绝,不依赖该域名真实可访问。
违反示例:请求失败,但审查记录没有 blocked.example:80 的具体拒绝目标和次数。
require_python3
require_curl
case_setup
case_run nonzero <<'CASE_COMMAND'
pvisor --overlaynet-deny blocked.example -- /bin/sh -c \
'curl --fail --silent --show-error --noproxy "" -x "$http_proxy" --max-time 2 http://blocked.example/'
CASE_COMMAND
bundle_contains network.intercepted.targets 'HTTP blocked.example:80'
bundle_contains network.intercepted.targets '"denied": 1'
H. Gateway 与记录¶
需要审计、模型路由或轨迹回放时使用这一组。H01 是 Gateway 配置示例,H02 把事件写成 JSONL。
S-DOC-041:H01 Gateway capture 完整组合¶
建议场景:适合接入 Gateway、模型路由或记录轨迹的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:Gateway 与 stage 成功建立。示例上游是占位地址,/bin/true 不发送模型请求;此例不验证对话内容。管理监听地址与运行记录中的 gateway_listen 不是同一个服务地址。
理由:为需要模型请求记录的 Agent 配置 Gateway。示例设置路由、管理监听端口、完整记录级别、会话头、诊断输出和 Markdown 投影,并保留 stage。
违反示例:Gateway/stage 未建立,或 gateway_listen 不是有效的 loopback 地址。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor \
--stage "$CASE_ROOT/gateway-stage" \
--gateway-mode capture \
--gateway-admin-listen "$CASE_GATEWAY_LISTEN" \
--gateway-level full \
--gateway-session-header X-Session-ID \
--gateway-debug \
--gateway-route 'name="default",upstream="https://example.com/v1"' \
-- "$CASE_TRUE"
CASE_COMMAND
record_get gateway_listen | grep -Eq '^127\.0\.0\.1:[0-9]+$'
bundle_expect network.interception.driver explicit-proxy
bundle_expect filesystem.state staged
S-DOC-042:H02 JSON 记录¶
建议场景:适合接入 Gateway、模型路由或记录轨迹的任务。
语义:指定文件非空,首行具有 JSON 对象形式。每行应是一个事件;本例只检查文件建立和首行外观。
理由:把本次运行事件写成 JSONL 文件。
违反示例:事件文件为空,或首行不是 JSON 对象。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --record-destination "$CASE_ROOT/events.jsonl" -- "$CASE_TRUE"
CASE_COMMAND
test -s "$PVISOR_CASE_ROOT/events.jsonl"
head -n 1 "$PVISOR_CASE_ROOT/events.jsonl" | grep -q '^{'
I. Spec 与控制面¶
已有自动化控制面或需要把 RunSpec 作为文件传递时使用这一组。I01 是 TOML 配置,I02 是 JSON 委托,I03 验证无扩展名文件。
S-DOC-043:I01 TOML config¶
建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。
语义:命令来自配置文件,无需在 CLI 重复;运行正常结束。
理由:把命令写进 TOML 后通过 --config 运行。手工执行前创建 pvisor.toml,内容为 [run] 下的 command = ["/bin/true"];脚本会预置此文件。
违反示例:配置文件中的命令被忽略,运行名称或终态不符。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --config ./pvisor.toml
CASE_COMMAND
bundle_expect run.state completed
bundle_expect run.agent true
S-DOC-044:I02 JSON RunSpec¶
建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:运行名称为 case-i02,run-result.json 非空。该委托路径当前只支持 host executor,不套用普通 Job 的 rootless safe profile;不要把此例视为隔离模式示例。
理由:执行已准备好的 JSON RunSpec,并把结果原子写入指定文件。手工运行前准备包含 run_id、agent 和 process invocation 的 run-spec.json;脚本预置的是运行 /bin/true 的 case-i02。
违反示例:委托运行没有生成结果文件,或被误记成隔离的普通 Job。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --spec ./run-spec.json --result-file ./run-result.json --stage ./delegated-stage
CASE_COMMAND
bundle_expect run.agent case-i02
bundle_expect run.executor.isolation host_process
test -s run-result.json
S-DOC-045:I03 无扩展名 spec¶
建议场景:适合从 TOML/JSON 文件或控制面执行 RunSpec 的任务。
语义:--config 正常读取 TOML 并完成运行,不要求文件名以 .toml 结尾。
理由:验证配置的识别不依赖扩展名。手工执行时把 I01 的 TOML 内容保存成 config-without-extension;脚本会预置该文件。
违反示例:相同 TOML 因文件没有扩展名而不能执行。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
pvisor --config ./config-without-extension
CASE_COMMAND
bundle_expect run.state completed
J. 复杂组合¶
这些是多项能力同时启用的回归示例:J01 偏 host 安全,J02 偏 VM,J03 偏容器。它们使用简短测试命令,不能代替真实 Agent 工作负载的验收;遇到问题时请拆回对应的 A–I 场景定位。
S-DOC-046:J01 host + persistent stage + deny-all + capture + limits¶
建议场景:适合上线前验证多项能力组合的端到端任务。
准备:Linux user/mount namespace 或 macOS Seatbelt 可用。
语义:原 workspace 不变;stage 记录 result.txt,结果保存 stdout 和资源请求,事件写入指定 JSONL 文件,网络标记为禁止连接。
理由:组合使用 host stage、禁止网络、输出采集、JSON 事件和资源限制。命令在隔离视图中写入一个结果文件。
违反示例:多能力组合让 result.txt 穿透工作区,或轨迹、资源、网络证据缺失。
require_python3
require_stage
case_setup
case_run success <<'CASE_COMMAND'
pvisor --name host-full \
--stage "$CASE_ROOT/host-full" \
--overlaynet-deny-all \
--stdio capture \
--record-destination "$CASE_ROOT/host-full/trajectory/events.jsonl" \
--memory 512MiB \
--max-processes 64 \
--overlayfs-max-size 2GiB \
--max-cpu-time 30s \
-- /bin/sh -c 'pwd; printf changed > result.txt'
CASE_COMMAND
bundle_expect run.agent host-full
test "$(bundle_get run.output.stdout)" = "$(record_get overlay.merged_dir)"
bundle_expect network.policy.mode no-network
bundle_expect safety.network_non_bypassable true
bundle_expect safety.filesystem_changes_staged true
bundle_contains filesystem.changes result.txt
bundle_expect resources.requested.memory_bytes 536870912
bundle_expect resources.requested.processes 64
bundle_expect resources.requested.cpu_time_ms 30000
test ! -e result.txt
test -s "$PVISOR_CASE_ROOT/host-full/trajectory/events.jsonl"
S-DOC-047:J02 VM + image rootfs + stage + OverlayNet + Gateway¶
建议场景:适合上线前验证多项能力组合的端到端任务。
准备:Linux;可访问 /dev/kvm;为脚本设置 PVISOR_CASE_IMAGE;PVISOR_CASE_AGENT 指向 guest 中也可执行的 Agent。
语义:VM 以请求的内存运行,保留 stage 和轨迹目录。是否产生模型对话取决于 Agent 是否真的调用 Gateway;当前断言不检查对话内容。
理由:在 VM 中运行真实 Agent,同时保留 stage、使用 smoltcp 网络、Gateway 和 JSONL 轨迹记录。需提供含 Agent 及其依赖的镜像,并把示例上游替换为实际服务。
违反示例:组合 VM 没有保留 stage/trajectory,或丢失 4GiB 内存请求。
require_python3
require_linux
require_kvm
require_image
require_agent
case_setup
case_run success <<'CASE_COMMAND'
pvisor --name vm-full \
--vm \
--rootfs "image=$CASE_IMAGE" \
--stage "$CASE_ROOT/vm-full" \
--overlaynet auto \
--gateway-mode capture \
--gateway-level dialogue \
--gateway-route 'name="default",upstream="https://example.com/v1"' \
--record-destination "$CASE_ROOT/vm-full/trajectory" \
--memory 4GiB \
--cpu 4 \
-- "$CASE_AGENT"
CASE_COMMAND
bundle_expect run.agent vm-full
bundle_expect run.executor.isolation virtual_machine
bundle_expect network.interception.driver vm-smoltcp
bundle_expect filesystem.state staged
bundle_expect resources.requested.memory_bytes 4294967296
test -d "$PVISOR_CASE_ROOT/vm-full/trajectory"
S-DOC-048:J03 Container + stage + read-only root + no network¶
建议场景:适合上线前验证多项能力组合的端到端任务。
准备:OCI runtime 可运行,并为脚本设置 PVISOR_CASE_CONTAINER_IMAGE。
语义:容器成功退出并留下 stage。命令为 /bin/true,不会生成文件变更或有意义的 stdout;本例不验证 stage 写入和网络阻断行为。
理由:在容器中组合持久 stage、只读 rootfs、隔离网络和 stdout 采集。只读 rootfs 与可写 stage 是不同层面的设置。
违反示例:组合 container 没有正常结束并保留 stage。
require_python3
require_container
case_setup
case_run success <<'CASE_COMMAND'
pvisor --container-runtime "$CASE_CONTAINER_RUNTIME" --name container-full \
--container-image "$CASE_CONTAINER_IMAGE" \
--container-read-only-rootfs \
--container-network none \
--stage "$CASE_ROOT/container-full" \
--stdio capture \
-- "$CASE_TRUE"
CASE_COMMAND
bundle_expect run.agent container-full
bundle_expect run.executor.kind container
bundle_expect run.state completed
bundle_expect filesystem.state staged
K. Job 的审查与生命周期¶
Job 是面向用户的核心对象。以下命令都直接使用 Job 的 stage 路径作为 selector,无需额外的 job 子命令。
S-DOC-049:K01 审查并只读查看暂存文件¶
准备:Linux user/mount namespace 可用。
语义:审查结果列出一个文件;只读视图能读到 staged,写入被拒绝,原 workspace 不变。
理由:运行后通过 status --review 查看变更,再用 inspect 读取暂存视图,并确认 inspect 无法写入。
违反示例:inspect 可以写暂存视图,或原工作区出现 note.txt。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/review-stage" -- /bin/sh -c 'printf staged > note.txt'
pvisor status --review --json "$CASE_ROOT/review-stage" > status.json
pvisor inspect "$CASE_ROOT/review-stage" -- /bin/cat note.txt
if pvisor inspect "$CASE_ROOT/review-stage" -- /bin/sh -c 'printf changed > note.txt'; then
exit 1
fi
CASE_COMMAND
stdout_has staged
test ! -e note.txt
python3 -c 'import json; d=json.load(open("status.json")); assert d["filesystem"]["changed_files"] == 1'
S-DOC-050:K02 选择性 apply 后丢弃剩余改动¶
准备:Linux user/mount namespace 可用。
语义:原 workspace 仅出现 one.txt;two.txt 从未进入 lower。
理由:只提交 one.txt,保留 two.txt 在 stage 中等待决定,然后显式丢弃剩余改动。
违反示例:仅选 one.txt 却把 two.txt 一并落地,或剩余 stage 未被丢弃。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/partial-stage" -- /bin/sh -c \
'printf one > one.txt; printf two > two.txt'
pvisor apply "$CASE_ROOT/partial-stage" --path one.txt
pvisor drop "$CASE_ROOT/partial-stage"
CASE_COMMAND
test "$(cat one.txt)" = one
test ! -e two.txt
record_expect overlay.state discarded "$PVISOR_CASE_ROOT/partial-stage"
S-DOC-051:K03 从已停止 Job fork¶
准备:Linux user/mount namespace 可用。
语义:子 Job 读到 inherited,但两个 Job 的变更都没有直接写入原 workspace。
理由:从源 Job 的暂存视图启动一个子 Job。子 Job 可以读取源改动,同时产生自己的独立变更。
违反示例:fork 读不到源 Job 变更,或子 Job 写入穿透原工作区。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/source-stage" -- /bin/sh -c 'printf inherited > inherited.txt'
pvisor fork "$CASE_ROOT/source-stage" -- /bin/sh -c \
'cat inherited.txt; printf child > child.txt' > child.out
CASE_COMMAND
test "$(cat child.out)" = inherited
test ! -e inherited.txt
test ! -e child.txt
bundle_contains filesystem.changes inherited.txt "$PVISOR_CASE_ROOT/source-stage"
bundle_contains filesystem.changes child.txt "$PVISOR_CASE_RECORDS"
S-DOC-052:K04 终止运行中的 Job¶
准备:Linux user/mount namespace 可用。
语义:Job 在睡眠结束前退出,status --json 报告 cancelled,不再处于 live 状态。
理由:让一个长时间运行的 Job 进入后台,然后按 stage 路径请求正常终止。
违反示例:kill 后 Job 继续 live,或终态不是 cancelled。
require_python3
require_rootless
case_setup
case_run success <<'CASE_COMMAND'
pvisor --stage "$CASE_ROOT/live-stage" -- /bin/sleep 30 > live.log 2>&1 &
job_pid=$!
for ((attempt=0; attempt<100; attempt++)); do
test -f "$CASE_ROOT/live-stage/run.json" && break
sleep 0.05
done
pvisor kill "$CASE_ROOT/live-stage"
if wait "$job_pid"; then exit 1; fi
pvisor status --json "$CASE_ROOT/live-stage" > stopped.json
CASE_COMMAND
python3 -c 'import json; d=json.load(open("stopped.json")); assert d["run"]["state"] == "cancelled" and d["live"] is False'
L. 已移除的环境命令¶
env 已移除,对应规格 S-DOC-053、S-DOC-054 已登记为 retired,ID 不再复用。
M. Replay 与交互终端¶
S-DOC-055:M01 离线准备回放前缀¶
语义:输出结果的 phase 为 prepared,历史命令没有创建 marker。
理由:用一个最小 mini-swe-agent 原生轨迹验证 replay --prepare-only。该模式解析前缀,不启动 Agent,也不执行历史工具。
违反示例:prepare-only 执行了历史命令,创建 marker,或报告回放过工具调用。
require_python3
case_setup
case_run success <<'CASE_COMMAND'
cat > trajectory.json <<'JSON'
{"trajectory_format":"mini-swe-agent-1.1","info":{"mini_version":"2.4.6"},"messages":[{"role":"assistant","content":"historical action","extra":{"response":{},"actions":[{"tool_call_id":"call-1","command":"printf should-not-run > marker"}]}},{"role":"tool","content":"old observation","extra":{"returncode":0}}]}
JSON
pvisor replay --agent mini-swe-agent --trajectory ./trajectory.json \
--after-step 1 --prepare-only \
--state-dir "$CASE_ROOT/replay-state" \
--output-dir "$CASE_ROOT/replay-output" > prepared.json
CASE_COMMAND
test ! -e marker
python3 -c 'import json; d=json.load(open("prepared.json")); assert d["phase"] == "prepared" and d["replayed_tool_calls"] == 0'
S-DOC-056:M02 TUI 保留命令输出并可打开 Log 面板¶
准备:Linux 和 Python 3。
语义:子命令正常退出;屏幕流中出现命令输出、底栏引导和 Log 面板。
理由:用伪终端执行 --tui,验证 Agent 输出、底栏引导键和 Ctrl-] → l 打开的浮动 Log 面板。交互终端由测试脚本提供。
违反示例:TUI 丢失命令输出,或 Ctrl-] 后看不到底栏和 Log 面板。
require_python3
require_linux
case_setup
case_run success <<'CASE_COMMAND'
python3 - <<'PY'
import fcntl, os, pty, select, struct, subprocess, termios, time
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 100, 0, 0))
env = os.environ.copy()
env['TERM'] = 'xterm-256color'
child = subprocess.Popen(
['pvisor', '--tui', '--', '/bin/sh', '-c', 'printf TUI_READY; sleep 1.5'],
stdin=slave, stdout=slave, stderr=slave, env=env, start_new_session=True,
)
os.close(slave)
screen = bytearray()
sent = False
deadline = time.monotonic() + 8
try:
while time.monotonic() < deadline:
ready, _, _ = select.select([master], [], [], 0.1)
if ready:
try:
screen.extend(os.read(master, 65536))
except OSError:
break
if not sent and b'TUI_READY' in screen:
os.write(master, b'\x1dl')
sent = True
if child.poll() is not None and not ready:
break
if child.poll() is None:
child.kill()
child.wait(timeout=2)
finally:
os.close(master)
assert child.returncode == 0, child.returncode
assert sent and b'TUI_READY' in screen
assert b'Ctrl-]' in screen and b'pVisor Review' in screen
print('TUI_READY status-bar log-panel')
PY
CASE_COMMAND
stdout_has 'TUI_READY status-bar log-panel'