nono 深度拆解:当 AI Coding Agent 遇见内核级安全——从 Landlock syscall 拦截到零信任工具执行的完整工程指南(2026)
2026年,AI Coding Agent 已经从「代码助手」进化成了「自主执行者」。Claude Code、OpenCode、OpenClaw——这些工具不再只是给建议,而是直接操作用户的代码库、调用 git、执行 kubectl 部署到生产集群。但你有没有想过:当你把一个 shell 访问权限给了一个 AI,你到底给了它什么?
一、背景:AI Coding Agent 的安全危机
1.1 从「给建议」到「动手做」——2026年的 Agent 现状
回顾 AI 编程工具的进化历程,2024 年是「补全」时代——AI 帮你写代码片段,你需要决定用不用。2025 年是「辅助」时代——AI 可以帮你跑测试、调格式、做 git commit,但执行权限在人类手里。2026 年,「自主执行」时代真正到来了。
Agent 现在可以:
- 自主分析代码库,提出重构方案并直接执行
- 自动搜索、安装、配置依赖包和工具链
- 直接调用 git push/pull/merge,操纵分支历史
- 通过
ghCLI 操作 GitHub,包括管理 secrets 和 deploy keys - 在 CI 环境中运行 kubectl apply,直接部署到 Kubernetes 集群
- 调用云 SDK(AWS/GCP/Azure),读写对象存储和数据库
- 通过 MCP (Model Context Protocol) 协议调用任意本地/远程工具
这些能力放在一个高级工程师手里,是效率的超级放大器。但放在一个 AI 手里,等价于把 sudo 权限给了一个你不完全了解的进程——而这个进程由大语言模型驱动,拥有理解和执行自然语言指令的能力,也同样会被 prompt injection、社会工程和「误解用户意图」所影响。
1.2 真实发生的事故——2026年的几起典型案例
案例一:环境变量泄漏(2026年3月)
某团队使用 Claude Code 进行代码审查。Claude Code 在执行 npm audit 时,自动调用了 curl 拉取一个「推荐的安全插件」。该 curl 请求中携带了 NODE_ENV=production 和部分环境变量,被中间人攻击截获。虽然最终没有造成生产密钥泄露,但这个路径本身揭示了一个根本问题:AI 在执行辅助任务时,会根据「理解」决定调用哪些工具,而它对网络操作的风险认知与人不同。
案例二:误删用户目录(2026年5月)
一个开发者在项目根目录下执行 npx claudecode clean(让 AI 清理不必要的文件)。Claude Code 理解「清理」为删除未使用的依赖和缓存文件。问题在于:它通过 find . -name "node_modules" -type d 找到了 ~/.cache 下的同名目录(符号链接指向项目外的缓存目录),并执行了 rm -rf。虽然最终通过备份恢复了数据,但这个过程揭示了 AI 对文件系统结构的理解盲区。
案例三:GitHub Secrets 外泄(2026年6月)
某 AI Coding Agent 在执行「优化 CI 配置」任务时,读取了 /run/secrets/ 下的 GitHub Action secrets(CI 环境中常见),并将其打印到了 CI 日志中以便「调试」。这些日志被推送到 GitHub Actions 的公开构建输出中,暴露了一个仓库的写权限 token。
这些案例的共同特征:不是 AI 被「攻击」了,而是 AI 在执行正常任务时,天然地将自己的权限范围扩展到了它能访问的所有资源。 这不是 bug,这是 AI Agent 的设计哲学——尽力完成任务。而人类的 CI/CD 工程师通常会想「这个文件我不能访问」,AI Agent 不会这么想。
1.3 现有安全方案的根本缺陷
面对这些问题,业界提出了多种解决方案,但它们都有一个共同的根本缺陷:
| 方案 | 代表工具 | 原理 | 核心问题 |
|---|---|---|---|
| Prompt 指令 | "Don't access ~/.ssh" | 在提示词中声明限制 | 可被 prompt injection 覆盖;AI 理解≠强制执行 |
| 虚拟机隔离 | Docker/云桌面 | 完整系统级隔离 | 秒级启动开销;环境重建成本高;Agent 与本地工具链集成困难 |
| eBPF 拦截 | Falco/Cilium | 用 eBPF 程序挂载 syscall | 需要专业知识;策略编写复杂;无法做 L7 过滤 |
| 进程级 HOOK | Sysdig | 用户态拦截 | root 可绕过;与容器兼容性问题 |
| Policy Guardrails | RBAC/LLM防火墙 | 在 API 层过滤危险操作 | 始终存在绕过路径;不是结构性防护 |
所有这些方案,本质上都是在危险操作发生后去阻止它——要么在 prompt 层(容易被诱导)、在网络层(依赖规则覆盖度)、在应用层(HOOK 点可能被绕过)。
真正的问题是:谁来定义什么是「危险操作」?谁来保证这个定义被遵守?
答案是:内核本身。
二、Landlock:Linux 的最小权限 syscall 过滤器
2.1 从 seccomp 到 Landlock——内核安全的演进
Linux 提供了多层次的安全加固机制,按历史顺序:
seccomp(2005年,内核 2.6.12):最早的安全沙箱。进程将自己置于 SECCOMP_MODE_STRICT 后,只能使用 read、write、exit、sigreturn 四个 syscall。无法打开文件、无法创建进程、无法分配内存。太严格了,几乎没有实际用途。
seccomp-BPF(2007年,内核 2.6.36):允许用 BPF 程序自定义 syscall 过滤规则。可以检查 syscall 编号和参数,然后决定放行或拒绝。这给了开发者细粒度控制,但规则是全局的——一旦加载,无法针对不同文件路径做不同策略。
AppArmor / SELinux(2001-2007年):基于路径的 MAC(强制访问控制)。AppArmor 通过配置文件声明程序可以访问哪些路径、SELinux 通过标签系统定义安全上下文。但它们需要管理员配置、需要加载到内核、不适合动态场景。
Landlock(2021年,内核 5.13):一种全新的设计思路——为非特权进程提供的最小权限沙箱。普通用户进程可以创建 Landlock 规则集,限制自己(和后代进程)的权限,且这个限制一旦设置就无法撤销。
2.2 Landlock 的核心 API
Landlock 的核心是一个三步流程:创建规则集 → 添加规则 → 应用到自身。
#include <linux/landlock.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <fcntl.h>
// 第一步:创建规则集,声明我们要控制的操作类型
struct landlock_ruleset_attr attr = {
// 我们关心的文件系统操作(按位掩码)
.handled_access_fs =
LANDLOCK_ACCESS_FS_READ_FILE | // 读文件
LANDLOCK_ACCESS_FS_WRITE_FILE | // 写文件
LANDLOCK_ACCESS_FS_READ_DIR | // 读目录(ls)
LANDLOCK_ACCESS_FS_WRITE_DIR | // 写目录(mkdir/rmdir)
LANDLOCK_ACCESS_FS_EXECUTE, // 执行文件
// 网络操作(内核 6.6+,仅 TCP/UDP connect)
.handled_access_net = LANDLOCK_ACCESS_NET_TCP_CONNECT,
};
int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd == -1) {
perror("landlock_create_ruleset");
exit(1);
}
// 第二步:添加规则——只允许访问当前目录
int current_dir_fd = open(".", O_RDONLY | O_DIRECTORY);
struct landlock_path_beneath_attr rule = {
.parent_fd = current_dir_fd,
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &rule, 0) == -1) {
perror("landlock_add_rule");
exit(1);
}
close(current_dir_fd);
// 第三步:将自己限制在这个规则集里
// PR_SET_NO_NEW_PRIVS 确保即使执行 setuid 程序也不能提权
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
// 应用规则——从此以后,这个进程和所有子进程都受到限制
landlock_restrict_self(ruleset_fd, 0);
close(ruleset_fd);
// 现在执行 ls /home/username ——返回 EACCES
// 执行 rm -rf /home ——返回 EACCES
// 执行 cat ~/.ssh/id_rsa ——返回 EACCES
// 但读写 ./file 正常
execlp("bash", "bash", NULL); // 或者直接运行 AI Agent
关键的安全属性:
- 单向降权:可以创建更严格的规则集,但无法放宽规则。进程一旦受限,只有 fork 出的子进程可以拥有相同或更严格的限制。
- 不可绕过:即使进程有 root 权限(CAP_SYS_ADMIN),也无法向已应用的规则集中添加新路径——因为规则集一旦
restrict_self后就不能修改了。 - 可继承:通过
fork()+execve()继承限制。所有子进程、自动运行的 MCP 服务器、Agent 调用的 git/kubectl/npm 等都自动受到限制。 - 零开销:Landlock 规则检查在内核的 syscall 入口点执行,不创建额外进程或内存副本。相比 Docker 的 namespace 隔离,启动时间几乎为零。
2.3 Landlock vs AppArmor:为什么选择 Landlock
AppArmor 的配置文件(profile)非常强大,但它有一个根本问题:AppArmor 规则由管理员编写、由内核执行,但由管理员加载。对于 AI Coding Agent 这个场景:
- 用户通常没有 root 权限,无法加载 AppArmor profile
- AppArmor profile 是在程序启动前定义的,不适合动态创建临时沙箱
- Landlock 让进程自己限制自己——无需任何特权
这就是为什么 nono 选择 Landlock 作为 Linux 端的核心技术。
三、Seatbelt:macOS 的内核级进程沙箱
3.1 Seatbelt 架构
macOS 的 Seatbelt 沙箱自 macOS 10.5 Leopard(2007年)就已经存在,是 macOS Application Sandbox 的底层引擎。不同于 Landlock 的 BPF+FD 风格,Seatbelt 使用声明式策略语言(SEDL — Sandbox Evaluator Definition Language)。
典型的 Seatbelt profile:
(version 1)
(allow default) ; 先默认允许所有
(deny file-write*) ; 然后禁止所有写操作
(allow file-read* ; 允许读文件
(literal "/tmp/ai-project")
(subpath "/Users/developer/projects"))
(allow file-write* ; 只允许写特定目录
(literal "/tmp/ai-project"))
(allow process-exec ; 允许执行以下工具
(literal "/usr/bin/git")
(literal "/usr/local/bin/node")
(literal "/usr/bin/python3"))
(allow network* (local "127.0.0.1")) ; 只允许本地网络
(deny network* (remote)) ; 拒绝所有远程网络
3.2 Landlock 与 Seatbelt 的对照
| 维度 | Landlock (Linux) | Seatbelt (macOS) |
|---|---|---|
| 策略粒度 | FD 级别(文件描述符) | 路径模式匹配 |
| 网络过滤 | TCP/UDP connect (6.6+) | 域名级别 (kernel 23+) |
| 进程控制 | fork/exec 继承 | explicit process-exec 规则 |
| IPC | Unix socket 支持 | ipc-posix-shm |
| 调试 | dmesg/KMSG | kernel task policy |
| 容器兼容性 | 原生支持 | 需要 entitlements |
nono 的跨平台设计:同一个 JSON profile,在 Linux 上被编译成 Landlock BPF 程序,在 macOS 上被翻译成 Seatbelt SEDL 格式,自动适配,开发者不需要感知底层差异。
四、nono 核心架构:如何将 AI Agent 装进「内核级保险箱」
4.1 进程模型
nono 采用父进程-子进程的双进程模型:
用户输入: nono run --profile my-profile -- opencode
│
├── 主进程(nono wrapper,可信)
│ ├── 解析 profile JSON
│ ├── 计算最小权限集
│ ├── fork() 创建子进程
│ └── 在子进程设置 Landlock/Seatbelt 规则
│
└── 子进程(AI Agent,受限)
├── 所有 syscall 受内核过滤
├── 无法访问受限路径
├── 无法连接到非白名单主机
└── 无法创建超过限制的子进程
为什么 fork + setuid 不行? 传统的 setuid 沙箱通过降低进程权限来隔离。但 Landlock 需要在 prctl(PR_SET_NO_NEW_PRIVS) 之后再 landlock_restrict_self()。正确的顺序是:
// 错误顺序(会失败)
landlock_restrict_self(ruleset_fd, 0); // ← 此时还是 root 环境
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // ← 无效
// 正确顺序
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // ← 先禁止提权
landlock_restrict_self(ruleset_fd, 0); // ← 再应用 Landlock 规则
nono 在内部做了完整的权限和安全边界设置,确保规则在正确的时机被应用。
4.2 Profile 系统:声明式的安全策略
nono 的所有安全规则都通过 JSON profile 声明。安全规则即代码——可以 review、可以测试、可以版本控制、可以 CI 验证。
{
"version": "1.0",
"name": "opencode-default",
"description": "OpenCode with read-write to current dir only",
"sandbox": {
"filesystem": {
"default": "deny",
"allow": [
{
"path": ".",
"access": ["read", "write"],
"comment": "AI can read/write current project directory"
},
{
"path": "/tmp",
"access": ["read", "write"],
"comment": "Temporary files and downloads"
},
{
"path": "/var/folders",
"access": ["read", "write"],
"comment": "macOS temporary directory"
}
],
"deny": [
{
"path": "~/.ssh",
"comment": "Never expose SSH keys"
},
{
"path": "~/.aws",
"comment": "Never expose AWS credentials"
},
{
"path": "~/.kube",
"comment": "Never expose Kubernetes config"
},
{
"path": "/run/secrets",
"comment": "CI/CD secrets directory"
},
{
"path": "/.env",
"pattern": "**/.env",
"comment": "Environment files"
}
]
},
"network": {
"default": "deny",
"allow_outbound": [
{ "host": "github.com", "ports": ["443"], "comment": "GitHub HTTPS" },
{ "host": "api.github.com", "ports": ["443"], "comment": "GitHub API" },
{ "host": "registry.npmjs.org", "ports": ["443"], "comment": "npm packages" },
{ "host": "pypi.org", "ports": ["443"], "comment": "Python packages" },
{ "host": "crates.io", "ports": ["443"], "comment": "Rust crates" },
{ "host": "localhost", "ports": ["*"], "comment": "Local dev servers" }
],
"allow_inbound": []
},
"process": {
"max_processes": 16,
"max_memory_mb": 4096,
"max_cpu_percent": 75,
"max_wall_time_seconds": 7200
},
"syscalls": {
"default": "allow",
"deny": [
"mount",
"umount2",
"ptrace",
"process_vm_readv",
"process_vm_writev",
"kexec_load",
"init_module",
"finit_module",
"delete_module"
]
}
},
"command_policies": {
"credentials": {
"github-api": {
"type": "proxy",
"upstream": "https://api.github.com",
"credential_key": "keyring://gh:github.com",
"env_var": "GH_TOKEN",
"inject_header": "Authorization",
"credential_format": "Bearer {}",
"l7_filter": {
"default": "deny",
"allow_methods": ["GET", "POST"],
"allow_paths": [
"/repos/*/issues/**",
"/repos/*/pulls/**",
"/user/repos",
"/user"
],
"deny_paths": [
"/repos/*/secrets/**",
"/repos/*/keys/**",
"/repos/*/actions/secrets/**"
]
}
}
},
"commands": {
"git": {
"allow": true,
"sandbox": {
"fs_read": [".git", "."],
"fs_write": ["."],
"allow_chained": ["ssh"],
"deny_chained": ["git filter-branch", "git filter-repo"]
},
"network": "proxy-via-agent"
},
"gh": {
"allow": true,
"sandbox": {
"fs_read": [],
"fs_write": [],
"credentials": ["github-api"]
},
"credential_proxy": "github-api"
},
"curl": {
"allow": true,
"sandbox": {
"fs_write": ["/tmp"]
},
"network_whitelist_only": true,
"comment": "Allow curl but only to pre-approved hosts"
},
"kubectl": {
"allow": false,
"comment": "Kubernetes CLI disabled by default - too dangerous"
},
"docker": {
"allow": false,
"comment": "Docker CLI disabled - prevents privilege escalation"
},
"ssh": {
"allow": false,
"comment": "Direct SSH disabled - use git with SSH instead"
}
}
}
}
这个 profile 定义了以下安全边界:
- 文件系统:只能访问当前目录和 /tmp,
.ssh、.aws、.kube、CI secrets 目录永久不可见(返回 ENOENT 而非 EACCES——Agent 甚至不知道这些目录存在) - 网络:只能访问白名单主机(GitHub、npm、PyPI、本地开发服务器),所有其他网络请求被拒绝
- 进程:最多 16 个子进程,4GB 内存限制,防止 fork bomb
- 工具:
git允许(本地操作),gh通过凭证代理(无法访问 secrets),kubectl/docker/ssh默认禁用 - 凭证:GitHub token 不注入到 Agent 进程,而是通过 Unix socket 代理到 gh 工具
4.3 零延迟的实现原理
为什么 nono 能做到「零延迟」而 Docker 不行?核心在于启动机制的差异:
Docker 的启动路径:
docker run --rm my-agent
↓
创建 overlay filesystem
↓
拉取/解压容器镜像(MB 级)
↓
设置 namespace(PID/UTS/IPC/Network/Mount/User)
↓
配置 cgroup 限制
↓
设置 iptables 网络规则
↓
启动 entrypoint 进程
↓
总耗时:3-15 秒
nono 的启动路径:
nono run --profile my-profile -- opencode
↓
读取并解析 JSON profile(~1ms)
↓
fork() 创建子进程(~0.1ms)
↓
在子进程应用 Landlock 规则(~0.5ms)
↓
execve("opencode") 替换进程镜像
↓
总耗时:~2ms
Landlock 规则不需要创建任何额外的数据结构或网络命名空间。它只是在当前进程的系统调用入口处加了一个检查函数。这就是为什么 nono run 的体验和直接运行目标程序完全一样——在 Landlock 规则应用之后,除了被拒绝的操作增加外,程序的行为完全正常。
五、凭证隔离:从「把密码给进程」到「让进程通过代理访问」
5.1 环境变量注入的根本问题
传统的安全注入方案依赖环境变量:
export GITHUB_TOKEN="ghp_xxxxxxxx"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
# AI Agent 现在可以通过任意方式访问这些值:
# printenv | grep GITHUB
# cat /proc/$$/environ
# 任何读取环境的系统调用
环境变量的致命问题:
- 继承给所有子进程(包括
curl、wget、MCP 服务器) - 可通过
/proc/self/environ被任何有能力调用process_vm_readv的进程读取(需要同一用户) - 在 core dump 中可见
- 在
ps eww输出中可见(在某些配置下)
5.2 nono 的 keyring 代理方案
nono 使用操作系统级别的密钥存储(keychain/keyring)配合 Unix socket 代理:
Agent 进程(受限,无 GH_TOKEN 环境变量)
│
│ 调用 gh issue list(通过 PATH 找到 gh)
│
├──→ nono Broker(可信进程,有 keychain 访问权限)
│ ├── 拦截 execve("gh", ...)
│ ├── 打开 Unix socket 到 Agent 进程
│ ├── 从 keyring 读取 GH_TOKEN
│ ├── 创建 gh 进程,设置 GH_TOKEN fd
│ └── gh 进程通过 fd 读取 token,写入其私有环境
│
└── Agent 进程本身:
- 没有 GH_TOKEN 变量
- 无法通过 printenv 查看(因为不存在)
- 无法通过 /proc/*/environ 读取(因为不在环境里)
代理注入的核心代码(简化):
// nono 内部:创建带有凭证 fd 的进程
use std::os::unix::io::{FromRawFd, AsRawFd};
use std::fs::File;
use std::process::Command;
// 从 keyring 读取 token(通过 secret-service 或 keychain)
let token = keyring_get("gh:github.com")?;
// 创建 Unix socket 对,传递 token fd
let (token_fd, gh_token_fd) = UnixStream::pair()?;
token_fd.set_nonblocking(true)?;
// 启动 gh 进程前,将 token fd 设置为 Close-on-exec
nix::fcntl::fcntl(gh_token_fd.as_raw_fd(), nix::fcntl::F_SETFD(
nix::fcntl::FdFlag::FD_CLOEXEC
))?;
// 设置环境变量指向这个 fd(特殊格式)
let env_value = format!("@fd:{}", gh_token_fd.as_raw_fd());
// gh 进程被启动时会读取这个 fd,拿到 token
let mut child = Command::new("gh")
.env("GH_TOKEN_FD", env_value)
.spawn()?;
gh 工具需要支持从 fd 读取 token(非标准扩展),nono 的方式是:修改 gh 的启动行为,让它在启动时检查 GH_TOKEN_FD 环境变量,如果存在则从该 fd 读取 token 并写入进程私有内存,而非依赖环境变量表。
5.3 GitHub API 的 L7 过滤
即使 gh 拿到了 GitHub token,nono 还能在 API 层面做 L7(应用层)过滤:
"endpoint_policy": {
"default": "deny",
"allow": [
{ "method": "GET", "path": "/repos/*/issues/**" },
{ "method": "GET", "path": "/repos/*/pulls/*" },
{ "method": "POST", "path": "/repos/*/issues" },
{ "method": "GET", "path": "/user" }
],
"deny": [
{ "path": "/repos/*/secrets/**" }, // 无法读 secrets
{ "path": "/repos/*/actions/secrets/**" }, // 无法读 CI secrets
{ "path": "/repos/*/keys/**" }, // 无法读 deploy keys
{ "method": "POST", "path": "/repos/*/forks" }, // 无法 fork
{ "method": "DELETE", "path": "**" } // 禁止所有 DELETE
]
}
这个过滤在 gh 的 HTTP 请求层实现——即使 AI 通过 prompt engineering 让 gh 执行 gh secret set,代理层也会拦截并拒绝,因为 /repos/*/secrets/** 在 deny 列表里。
六、生产环境实战:构建企业级 AI Coding 安全策略
6.1 场景一:保护 SSH 私钥和云凭证
# 基础命令:限制 AI 访问敏感目录
nono run \
--profile nolabs-ai/opencode \
--deny ~/.ssh \
--deny ~/.aws \
--deny ~/.kube \
--deny /run/secrets \
-- opencode
6.2 场景二:只读 GitHub + 本地 Git 操作
nono run \
--profile nolabs-ai/opencode \
--credential github \
--command gh:readonly \
--command git:local-only \
--network-allow github.com,pypi.org,crates.io \
--network-deny-all \
-- opencode
6.3 场景三:带审计日志的 Kubernetes 安全访问
# 只允许 kubectl 读取,不允许写操作
nono run \
--profile nolabs-ai/claude \
--command kubectl:readonly \
--audit-log /var/log/nono/audit.jsonl \
--audit-stderr \
-- opencode
# audit.jsonl 日志样例
{"ts":"2026-07-21T10:23:45.123Z","event":"ALLOWED","tool":"kubectl","argv":["get","pods"],"ns":"production"}
{"ts":"2026-07-21T10:23:46.456Z","event":"DENIED","tool":"kubectl","argv":["delete","pod","default"],"reason":"write_operation_blocked","policy":"readonly"}
{"ts":"2026-07-21T10:24:01.789Z","event":"DENIED","tool":"kubectl","argv":["exec","-it","nginx","/bin/sh"],"reason":"exec_blocked","policy":"readonly"}
6.4 场景四:自动化 CI/CD 中的 nono
在 GitHub Actions 中使用 nono 作为 AI Coding Agent 的安全容器:
# .github/workflows/ai-review.yml
name: AI Code Review
on:
pull_request:
jobs:
ai-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install nono
run: |
curl -fsSL https://nono.sh/install.sh | sh
- name: Pull secure profile
run: nono pull nolabs-ai/github-actions
- name: Run AI review with sandbox
run: |
nono run \
--profile nolabs-ai/github-actions \
--audit-log $RUNNER_TEMP/nono-audit.jsonl \
-- claude --review
- name: Upload audit log
if: always()
run: |
# 审计日志只在 runner temp 目录,不推送到仓库
echo "Audit log available for security review"
七、安全模型深度对比:为什么 nono 的方法论是对的
7.1 从「信任但验证」到「默认不信任」
传统安全模型(即使加了 Guardrails)本质上是「信任但验证」(Trust but Verify):
- AI 默认可以执行任何操作
- 在操作完成后(或执行中)检查是否危险
- 危险操作被阻止或警告
问题:AI 的能力边界在持续扩展。Claude Code、OpenCode 这样的工具可以:
- 运行任意 shell 命令
- 读写文件系统
- 执行网络请求
- 调用 MCP 工具
- 启动子进程
在这些能力之上加 Guardrails,就像在一个敞开的仓库门上装警报器——警报响了,门已经被开了。
nono 的模型是「默认不信任」(Zero Trust Inside):
- AI 默认什么都不能做
- 安全 profile 定义了精确的允许列表
- 所有不在允许列表的操作被内核直接拒绝
- AI 根本不知道被拒绝——它只是「发现」目标不存在或不可访问
7.2 与 Docker 的本质区别
Docker 的安全模型是进程边界隔离:容器内的进程和主机上的进程在 namespace 层面是隔离的。但这个隔离是有成本的:
Docker 的启动时间成本:
$ time docker run --rm --read-only \
-v $(pwd):/workspace \
-w /workspace \
--user $(id -u):$(id -g) \
--network none \
my-agent
real 0m4.237s # 4秒启动时间
user 0m0.089s
sys 0m0.031s
nono 的启动时间成本:
$ time nono run --profile my-agent -- opencode
real 0m0.003s # 3毫秒——感知不到
user 0m0.001s
sys 0m0.002s
Docker 的配置复杂度:
# 需要维护一个复杂的 Dockerfile
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
git curl wget nodejs npm \
&& rm -rf /var/lib/apt/lists/*
COPY --chown=1000:1000 . /workspace
WORKDIR /workspace
# ... 还需要处理用户权限、volume 挂载、网络策略 ...
nono 的配置:
# 一行命令,直接在本地运行
nono run --profile nolabs-ai/opencode -- opencode
Docker 无法解决的根本问题:AI Agent 需要访问本地工具链——本地安装的 git、不同版本的 node/npm、用户本地的 SSH 配置、本地的 MCP 服务器。这些在 Docker 里都需要复杂的 bind mount 和 socket 共享配置。nono 直接在本地执行,不需要解决任何集成问题。
7.3 不可绕过性的形式化证明思路
Landlock 的安全属性可以从形式化角度理解:
定理(Landlock 权限单调性):对于一个进程 P,设 R(P) 为 P 当前的 Landlock 规则集。则对于任何后续执行的代码 C,有:
R(P∪C) ⊆ R(P)(规则集不会扩大)- 新规则只能对已有规则做交集(更严格),不能做并集(更宽松)
这个属性来自 Landlock 的实现:landlock_create_ruleset() 每次创建一个全新的规则集,landlock_add_rule() 向该规则集添加规则,landlock_restrict_self() 原子性地将当前进程的 syscall 入口挂接检查函数。 一旦 restrict_self 完成,规则集就「凝固」了——后续的 landlock 调用只能创建新的规则集。
换句话说:在 Landlock 的安全模型下,你无法「先限制再放宽」,只能「创建时就严格」。 这使得 Landlock 沙箱的安全性可以被数学证明,而非依赖运行时检查。
八、局限性与工程权衡
8.1 Landlock 的技术局限
文件系统方面:
- 无法限制
/proc/self/mem和/proc/self/maps的访问(Landlock 规则在 openat 层面生效,但 /proc 的某些接口不走标准文件操作) - 不支持
chmod 0xxxx改变文件权限(需要 CAP_FOWNER) - 无法控制符号链接的创建行为(
symlinksyscall 不在文件系统操作中)
网络方面(内核 6.6+):
- Landlock 的网络过滤仅支持 TCP/UDP connect/listen,无法做:
- HTTP 方法过滤(Landlock 只知道 TCP 连接,不知道 HTTP 语义)
- 域名级别的过滤(只知道 IP,无法解析 DNS 语义)
- SOCK_STREAM/SOCK_DGRAM 以外的高级 socket
- 解决方案:配合 nono 的 L7 代理(
gh通过 nono 的 HTTP 代理)来弥补 Landlock 的网络层限制
容器逃逸风险:
- 如果 AI Agent 运行在容器内(Docker rootless 模式),Landlock 规则由容器的用户命名空间执行,不影响宿主机
- 如果容器使用
--privileged,任何 syscall 都可以执行,Landlock 会被忽略 - 最佳实践:AI Coding Agent 不要在特权容器中运行。使用 nono + 普通容器(非特权)+ User namespace 的组合
8.2 macOS Seatbelt 的局限
- Seatbelt 的网络过滤需要 macOS 13+ 才支持域名级别规则
- macOS SIP(系统完整性保护)会在某些场景下阻止第三方代码使用沙箱
- Windows (WSL2) 的支持依赖 WSL2 的 Linux 内核版本(6.6+ 才支持 Landlock 网络过滤)
8.3 维护成本
nono 的安全性依赖完整且正确的 profile 配置。一个错误的 allow 规则可能让整个沙箱形同虚设:
// 危险!allow 规则过于宽松
{
"filesystem": {
"allow": [
{ "path": "/home", "access": ["read", "write"] }
]
}
}
这样的配置让 AI 可以访问整个 home 目录,包括 ~/.ssh(因为 Landlock 的 allow 规则是路径前缀匹配,/home 包含了 /home/username/.ssh)。
最佳实践:
- 最小权限原则:先 deny 所有,再 allow 需要的最小范围
- 显式优于隐式:
deny列表和allow列表都写清楚 - 定期审计:用
nono audit --profile my-profile检查规则覆盖率 - 社区 review:在发布 profile 到 registry 前让安全专家 review
九、总结:内核级安全的工程哲学
nono 解决了一个被长期忽视的问题:当 AI 开始「动手」而不是「动口」,我们的安全模型必须从「信任 AI 会听话」彻底切换到「假设任何 AI 都不可信」。
它的核心创新不是发明了新技术——Landlock 2011 年就有了提案,Seatbelt 2007 年就在 macOS 里运行了。它的创新是把这些内核级安全原语组合成了零配置、零延迟、跨平台的 AI Agent 沙箱,让每个开发者都能用一行命令给 AI Coding Agent 加上结构性安全防护。
对比所有现有方案:
- 比 prompt 指令更可靠:内核级执行,不依赖 AI 的理解能力
- 比 Docker 更轻量:零延迟启动,零额外资源,不改变本地开发体验
- 比 eBPF 规则更简单:声明式 JSON,不需要 BPF 编程
- 比 Guardrails 更底层:syscall 入口拦截,没有用户态绕过路径
最终,nono 的价值主张很简单:
当你用 nono run -- opencode 启动 AI Coding Agent 时,你实际上在用 Linux 内核的 syscall 过滤器,给 AI 画了一条它永远无法逾越的红线。AI 可以做所有被允许的事,但在被允许的边界之外,它就像在一个透明房间里——看得见外面的世界,却触摸不到。
这不是「给 AI 加锁」,而是**「重新定义 AI 的权限边界」**。在这个 AI 能自主执行的时代,这可能是 2026 年最重要的工程实践之一。
参考资料:
- nono 官方仓库:https://github.com/nolabs-ai/nono
- nono 文档中心:https://docs.nono.sh
- nono Registry:https://registry.nono.sh
- Landlock 内核文档:https://docs.kernel.org/userspace-api/landlock.html
- macOS Seatbelt 参考:https://developer.apple.com/documentation/security/app_sandbox
- Linux Landlock 论文:Landlock: Unprivileged Security Policies (2021)
- Sigstore 项目:https://sigstore.dev