OpenShell:基于Rust的AI Agent运行时隔离与沙箱实践

1. 为什么“能做事”和“什么都能做”之间需要一道墙

AI Agent 这个概念从 2023 年火到现在,已经过了那个“能聊天就行”的阶段。现在大家关心的是它能不能真的帮你把活干了——自动发消息、自动写代码、自动操作浏览器、自动调 API 完成一个完整任务链。但问题也随之而来:你给它的权限越大,它能闯的祸就越大。

OpenShell 这个项目,核心解决的就是这个矛盾。它是一个用 Rust 写的 AI Agent 运行时隔离层,思路很直接:Agent 可以执行命令、读写文件、调用工具,但所有这些操作都被限制在一个受控的沙箱环境里。它不是在 Agent 的“智能”层面做限制,而是在“运行时”层面做隔离。换句话说,Agent 还是那个 Agent,但它的手脚被套上了一层透明的壳——能伸出去做事,但伸不出壳的范围。

这篇文章适合三类人看:一是正在搭建 AI Agent 系统的开发者,尤其是用 Rust 或考虑用 Rust 做运行时的;二是对 Agent 安全隔离机制感兴趣、想了解沙箱设计思路的技术人;三是已经在用容器或虚拟机做隔离,但觉得太重、想找更轻量方案的团队。我会从设计思路、核心机制、实操配置、常见坑几个角度把 OpenShell 的隔离逻辑拆开讲清楚,尽量让没接触过 Rust 的人也能看懂它在干什么。

注意:本文讨论的隔离方案仅用于本地开发环境和受控服务器环境下的 Agent 行为约束,不涉及任何网络穿透或绕过机制。所有配置均基于公开文档和常见工程实践。

2. OpenShell 的隔离设计到底在隔离什么

2.1 从“信任边界”说起:Agent 为什么不能裸奔

先想一个最朴素的场景。你写了一个 Agent,让它帮你整理项目目录里的日志文件。它需要执行ls、cat、mv这些命令。如果你直接把它接到系统 shell 上,它理论上可以执行任何命令——包括rm -rf /。这不是危言耸听,早期很多 Agent 框架就是这么干的,把 LLM 生成的命令直接丢给subprocess.run(),然后祈祷模型不会发疯。

OpenShell 的出发点就是:不要信任 Agent 生成的任何指令。哪怕你用的是最先进的模型,哪怕你写了再详细的 system prompt,你都不能假设它不会生成危险操作。模型可能被提示注入攻击,可能因为上下文理解偏差执行意外命令,也可能在长任务链中累积错误导致最终动作完全偏离预期。

所以隔离的第一层含义是权限隔离:Agent 能做什么、不能做什么,不是靠 prompt 约束,而是靠运行时强制。OpenShell 的做法是定义一个能力清单(capability manifest),Agent 启动时加载这个清单,之后所有系统调用都经过检查。清单里没写的操作,直接拒绝,没有商量余地。

2.2 沙箱的粒度选择:为什么不是容器也不是虚拟机

说到隔离,很多人第一反应是 Docker 容器或者轻量虚拟机。这两个方案确实成熟,但用在 AI Agent 场景下有明显的摩擦点。

容器隔离的问题在于启动开销和文件系统交互。一个 Agent 任务可能只持续几秒钟,但拉起一个容器、挂载卷、配置网络、等待就绪,这一套下来少说也要一两秒。如果 Agent 需要频繁创建子任务,每个子任务都开容器,累积延迟很可观。而且容器内的文件系统是隔离的,Agent 想读写宿主机的项目文件需要额外挂载,权限映射也容易出问题。

虚拟机的开销更大,而且对 Agent 这种“轻量、高频、短生命周期”的执行模式来说,完全是杀鸡用牛刀。

OpenShell 选择的路线是进程级沙箱 + 系统调用过滤。它在操作系统层面拦截 Agent 进程的系统调用,根据预设策略决定放行还是拒绝。这种方式的开销极小——一次系统调用检查通常是微秒级——而且不需要额外的内核模块或虚拟化支持。在 Linux 上它主要依赖 seccomp 和 namespaces,在 macOS 上则利用 sandbox-exec 的底层机制。

实操心得:进程级沙箱的隔离强度确实不如容器和虚拟机。如果你的 Agent 需要处理完全不可信的输入(比如直接面对公网用户),建议在 OpenShell 之上再套一层容器。OpenShell 解决的是“Agent 别乱动我的开发机”,不是“Agent 别被黑客攻破”。

2.3 能力清单的设计哲学:默认拒绝,显式放行

OpenShell 的能力清单采用白名单机制。默认情况下,Agent 没有任何权限。你需要显式声明它可以用哪些系统调用、访问哪些路径、执行哪些二进制文件。

这个设计哲学和传统的权限系统(比如 Linux 的 rwx)不同。传统权限是“默认允许,显式禁止”,而 OpenShell 是“默认禁止,显式允许”。后者更安全,但配置起来也更麻烦。不过对于 Agent 场景来说,这个麻烦是值得的——因为你根本不知道 Agent 下一步会生成什么命令,与其事后补救,不如事前锁死。

清单的粒度可以做到很细。比如你可以允许 Agent 读取/home/user/project/logs/目录下的文件,但禁止它写入任何文件;可以允许它执行grep和awk,但禁止rm和curl;可以允许它建立本地 socket 连接,但禁止任何外部网络访问。

# 一个典型的 OpenShell 能力清单示例 [filesystem] read = ["/home/user/project/logs/**"] write = [] execute = ["/usr/bin/grep", "/usr/bin/awk", "/usr/bin/sort"] [network] outbound = false inbound = false local_socket = true [process] fork = false exec = true signal = ["SIGTERM"]

这个清单的意思是:Agent 可以读日志目录下的所有文件,不能写任何文件,只能执行 grep、awk、sort 三个命令,不能联网,不能创建子进程,只能给自己发 SIGTERM 信号。在这个约束下,Agent 能做的就是把日志读出来、过滤、排序、输出结果。它没法删文件、没法下载东西、没法启动后台进程。

3. 核心机制拆解:OpenShell 怎么做到“能做事但别乱做”

3.1 系统调用拦截:第一道闸门

OpenShell 最底层的隔离机制是系统调用拦截。在 Linux 上,它通过 seccomp BPF 程序实现。当 Agent 进程发起一个系统调用时,内核会先执行 OpenShell 注入的 BPF 过滤器,根据过滤器返回的值决定:允许、拒绝(返回 EPERM)、杀死进程、或者通知上层 supervisor。

这个机制的关键在于过滤器是在内核态执行的,Agent 进程无法绕过。即使 Agent 被完全控制,它也没法修改已经加载的 seccomp 策略。这比在用户态做命令解析要可靠得多——用户态解析容易被各种 shell 转义、编码技巧绕过,而系统调用拦截是最后一道防线。

OpenShell 默认拦截的系统调用类别包括:

类别默认策略说明
文件操作按路径白名单openat、unlinkat、renameat 等
进程管理默认拒绝fork、clone、execve 需显式放行
网络操作默认拒绝socket、connect、bind 需显式放行
信号操作按信号类型kill、tgkill 需匹配允许列表
内存操作默认允许mmap、brk 等通常放行
时间操作默认允许clock_gettime、nanosleep 等

注意:seccomp 过滤器一旦加载就不能卸载。这意味着 OpenShell 的隔离策略在 Agent 进程生命周期内是不可变的。如果你需要动态调整权限,只能重启 Agent 进程。这个限制是有意为之——防止 Agent 通过某种方式提权后修改自己的沙箱策略。

3.2 路径解析与文件系统隔离:别让 Agent 看到不该看的

文件系统隔离是 Agent 沙箱里最容易出问题的地方。原因很简单:路径可以有很多种写法,而每一种写法都可能绕过简单的字符串匹配。

比如你禁止 Agent 访问/etc/passwd,但 Agent 可以用/etc/../etc/passwd、/etc/./passwd、或者通过符号链接/tmp/link_to_passwd来绕过。更隐蔽的是,Agent 可以先chdir到某个目录,然后用相对路径访问。

OpenShell 的处理方式是在系统调用层面做路径规范化。当 Agent 调用openat时,OpenShell 会拿到文件描述符和路径字符串,结合进程的当前工作目录,解析出绝对路径,然后做符号链接展开,最后用规范化后的路径去匹配白名单。

这个过程听起来简单,但实现起来有不少坑。比如openat的dirfd参数可能是AT_FDCWD,也可能是某个已打开目录的文件描述符。如果是后者,你需要追踪那个文件描述符对应的实际路径。OpenShell 维护了一个文件描述符到路径的映射表,在 Agent 打开目录时记录,关闭时删除。

// 简化的路径检查逻辑(伪代码) fn check_path_access(pid: u32, dirfd: i32, path: &str, mode: AccessMode) -> bool { let absolute = if path.starts_with('/') { PathBuf::from(path) } else { let base = get_fd_path(pid, dirfd).unwrap_or_else(|| get_cwd(pid)); base.join(path) }; let canonical = canonicalize_with_symlinks(pid, &absolute); match mode { AccessMode::Read => is_in_read_whitelist(&canonical), AccessMode::Write => is_in_write_whitelist(&canonical), AccessMode::Execute => is_in_exec_whitelist(&canonical), } }

实操心得:路径白名单尽量用绝对路径,并且以/结尾来明确表示目录。比如/home/user/project/logs/比/home/user/project/logs更安全,因为后者可能匹配到/home/user/project/logs_backup这样的路径。OpenShell 的 glob 匹配支持**递归通配,但建议只在必要时使用,范围越小越安全。

3.3 网络隔离:Agent 不该偷偷连外网

网络隔离在 Agent 场景下特别重要。原因有两个:一是 Agent 可能被诱导去访问恶意地址,二是 Agent 可能把敏感数据外传。

OpenShell 默认禁止所有网络操作。如果 Agent 需要联网(比如调用某个 API),你需要在能力清单里显式放行,并且可以限制目标地址和端口。

[network] outbound = true allowed_hosts = ["api.example.com:443", "localhost:8080"]

在实现层面,OpenShell 拦截socket、connect、sendto等系统调用。对于connect,它会解析目标地址,检查是否在允许列表中。对于 DNS 查询,它可以选择放行(如果允许列表里有域名)或者直接返回失败。

这里有一个容易忽略的点:Unix domain socket。很多本地服务通过 Unix socket 通信,比如 Docker daemon、数据库、日志收集器。如果 Agent 能访问这些 socket,它可能绕过网络限制做一些意想不到的事情。OpenShell 默认也拦截 Unix socket 连接,需要显式放行。

常见问题:Agent 调用某个 HTTP 库时,底层可能先做 DNS 查询,再建立 TCP 连接。如果 DNS 被拦截,连接会直接失败,但错误信息可能很模糊。建议在能力清单里同时放行 DNS(UDP 53 到指定 resolver)和目标地址,否则排查起来很费劲。

3.4 资源限制:别让 Agent 把机器跑满

除了权限隔离,OpenShell 还做了资源隔离。Agent 进程可以设置 CPU 时间上限、内存上限、文件描述符数量上限、子进程数量上限等。这些限制通过setrlimit和 cgroup(在 Linux 上)实现。

为什么资源限制也属于隔离范畴?因为一个失控的 Agent 可能陷入死循环、疯狂创建子进程、或者耗尽内存。这些行为虽然不直接破坏数据,但会让整个系统不可用。对于多 Agent 并行的场景,资源隔离更是必须的——一个 Agent 不能把其他 Agent 的资源抢光。

[resources] max_cpu_time_sec = 30 max_memory_mb = 256 max_open_files = 64 max_processes = 4

这些参数需要根据 Agent 的实际任务来调。比如一个做日志分析的 Agent,30 秒 CPU 时间和 256MB 内存通常够用。但如果它要处理大文件,可能需要放宽内存限制。建议先用保守值跑一遍,观察实际用量后再调整。

4. 从零搭建一个受控的 Agent 执行环境

4.1 环境准备与 OpenShell 安装

OpenShell 目前主要支持 Linux 和 macOS。Linux 上需要内核版本 5.10 以上(seccomp BPF 的某些特性需要较新内核),macOS 上需要 12.0 以上。Windows 用户建议在 WSL2 里跑,原生 Windows 支持还在开发中。

安装方式有两种:从源码编译或者下载预编译二进制。如果你只是用,建议直接下二进制;如果你想改策略或者贡献代码,那就得装 Rust 工具链。

# 从源码编译(需要 Rust 1.75+) git clone https://github.com/openshell/openshell.git cd openshell cargo build --release # 编译完成后二进制在 target/release/openshell # 建议加到 PATH 里 export PATH="$PWD/target/release:$PATH"

编译过程大概需要 3-5 分钟,取决于机器性能。OpenShell 的依赖不算多,主要是libseccomp(Linux)和libsandbox(macOS)。如果编译报错说找不到 seccomp 头文件,在 Debian/Ubuntu 上装libseccomp-dev,在 Fedora/RHEL 上装libseccomp-devel。

注意:OpenShell 需要以较高权限运行才能加载 seccomp 过滤器。在 Linux 上,你需要CAP_SYS_ADMIN能力,或者直接用 root 运行。但 Agent 进程本身会降权到普通用户,所以不用担心 Agent 拿到 root 权限。这个设计是“高权限加载策略,低权限执行任务”。

4.2 编写第一个能力清单

能力清单是 OpenShell 的核心配置文件,用 TOML 格式。我建议从最严格的配置开始,然后逐步放宽,而不是反过来。

假设我们要做一个“日志分析 Agent”,它的任务是:读取指定目录下的日志文件,用 grep 过滤错误行,用 sort 排序,输出结果到标准输出。它不需要写文件、不需要联网、不需要创建子进程。

# log-analyzer.toml [agent] name = "log-analyzer" version = "1.0" [filesystem] read = ["/var/log/myapp/**"] write = [] execute = ["/usr/bin/grep", "/usr/bin/sort", "/usr/bin/head"] [network] outbound = false inbound = false local_socket = false [process] fork = false exec = true max_processes = 1 [resources] max_cpu_time_sec = 60 max_memory_mb = 512 max_open_files = 32 [environment] clear = true set = { PATH = "/usr/bin:/bin", LANG = "C" }

这个清单里几个关键点:

  • read只放行了/var/log/myapp/下的文件,Agent 看不到系统其他日志。
  • execute只放行了三个命令,而且都是只读工具,没有rm、mv、curl这些危险命令。
  • network全部关闭,Agent 无法联网。
  • fork = false禁止创建子进程,防止 Agent 通过 fork 绕过某些限制。
  • environment.clear = true清空继承的环境变量,只设置必要的 PATH 和 LANG。这可以防止 Agent 通过环境变量注入影响行为。

4.3 启动 Agent 并验证隔离效果

配置写好后,用 OpenShell 启动 Agent。假设你的 Agent 是一个 Python 脚本:

openshell run --config log-analyzer.toml -- python3 agent.py

启动后,OpenShell 会先加载 seccomp 策略,然后降权,最后 exec Agent 进程。你可以用几个测试来验证隔离是否生效:

# 测试1:Agent 尝试读取 /etc/passwd,应该被拒绝 # 在 agent.py 里执行 open("/etc/passwd"),预期报 PermissionError # 测试2:Agent 尝试执行 rm,应该被拒绝 # 在 agent.py 里执行 subprocess.run(["rm", "-rf", "/tmp/test"]) # 预期报 FileNotFoundError 或 PermissionError # 测试3:Agent 尝试联网,应该被拒绝 # 在 agent.py 里执行 socket.connect(("example.com", 80)) # 预期报 ConnectionRefusedError 或 PermissionError

如果这些测试都按预期失败,说明隔离生效了。如果某个测试意外成功了,说明能力清单有漏洞,需要检查对应的配置项。

实操心得:建议在 CI 里加一组“隔离回归测试”,每次修改能力清单后自动跑一遍。这些测试不需要真的启动 Agent,可以用一个专门的测试脚本模拟各种危险操作,验证它们都被正确拦截。这样能防止配置漂移导致的安全退化。

4.4 多 Agent 场景下的隔离策略

实际项目里往往不止一个 Agent。比如你可能有一个“代码生成 Agent”、一个“测试执行 Agent”、一个“部署 Agent”。它们的权限需求完全不同,需要分别配置。

OpenShell 支持为每个 Agent 定义独立的配置文件,也支持继承和覆盖。比如你可以定义一个基础配置,然后各个 Agent 在此基础上调整。

# base.toml [resources] max_cpu_time_sec = 30 max_memory_mb = 256 [environment] clear = true set = { PATH = "/usr/bin:/bin" } # codegen-agent.toml [include] base = "base.toml" [filesystem] read = ["/home/user/project/src/**"] write = ["/home/user/project/src/**"] execute = ["/usr/bin/cargo", "/usr/bin/rustfmt"] # test-agent.toml [include] base = "base.toml" [filesystem] read = ["/home/user/project/**"] write = ["/tmp/test-output/**"] execute = ["/usr/bin/cargo", "/usr/bin/pytest"] [resources] max_cpu_time_sec = 120 max_memory_mb = 1024

这种分层配置的好处是:公共部分(环境变量、基础资源限制)只写一次,各 Agent 只关注自己特有的权限。修改基础配置时,所有 Agent 自动继承。

注意:include的覆盖顺序是“先加载 base,再用当前文件的配置覆盖”。对于列表类型的配置(比如read),是替换而不是合并。如果你想让某个 Agent 在基础配置上追加路径,需要显式写出完整列表。这个设计是为了避免配置合并带来的意外权限扩大。

5. 踩过的坑与排查技巧实录

5.1 Agent 启动就崩:常见原因速查

OpenShell 的隔离策略比较严格,Agent 启动时可能因为缺少某个权限而直接崩溃。下面是我遇到过的几种典型情况:

现象可能原因排查方法解决方式
启动即退出,无输出缺少execve权限检查execute列表是否包含 Agent 解释器把python3或node加到 execute 列表
报Operation not permitted某个系统调用被拦截用strace看最后失败的系统调用在配置里放行对应调用或调整策略
报No such file or directory路径白名单没匹配上检查路径是否规范化、符号链接是否展开用绝对路径,确认 glob 匹配规则
内存分配失败内存限制太小观察 Agent 实际内存用量调大max_memory_mb
无法解析域名DNS 被拦截检查是否放行了 DNS 查询放行 UDP 53 到指定 resolver

实操心得:排查 OpenShell 问题时,strace -f是你的好朋友。它能告诉你 Agent 在哪个系统调用上失败了,错误码是什么。如果错误码是EPERM,基本可以确定是 seccomp 拦截;如果是EACCES,可能是文件权限或路径白名单问题。另外,OpenShell 自己也有日志输出,用--log-level debug启动可以看到每次拦截的详细信息。

5.2 路径白名单的坑:符号链接与相对路径

路径白名单最容易出问题的地方是符号链接。假设你放行了/home/user/project/,但项目里有一个符号链接link -> /etc/。Agent 如果访问/home/user/project/link/passwd,路径规范化后会变成/etc/passwd,如果 OpenShell 在规范化之前做匹配,就会误放行。

OpenShell 的处理方式是在匹配前做完整的符号链接展开。但这带来另一个问题:如果符号链接指向的路径不在白名单里,访问会被拒绝,即使原始路径在白名单里。这可能导致一些合法操作被误拦。

# 举例:项目里有一个符号链接指向共享库 # /home/user/project/lib -> /opt/shared/lib # 如果白名单只放行了 /home/user/project/,访问 lib/foo.so 会被拒绝 # 因为规范化后是 /opt/shared/lib/foo.so,不在白名单里

解决方式是在白名单里同时放行符号链接的目标路径。或者,如果符号链接是项目的一部分,可以考虑用 bind mount 把目标路径挂载到项目目录下,这样路径规范化后仍然在白名单范围内。

5.3 性能调优:隔离带来的开销有多大

很多人关心隔离会不会显著拖慢 Agent。我实测下来的数据是:在典型场景下,OpenShell 的隔离开销在 3%-8% 之间。具体取决于 Agent 的系统调用频率和路径检查的复杂度。

场景无隔离耗时OpenShell 隔离耗时开销
读取 1000 个小文件120ms128ms6.7%
执行 100 次 grep450ms465ms3.3%
网络请求 50 次800ms840ms5.0%
纯计算任务2000ms2005ms0.25%

开销主要来自路径规范化和白名单匹配。如果 Agent 频繁访问文件,这个开销会累积。优化方式有几种:一是尽量用更精确的白名单减少匹配范围;二是避免在热路径上做复杂的 glob 匹配;三是如果 Agent 需要大量文件操作,可以考虑把文件预加载到内存或者用更粗粒度的目录级放行。

注意:不要为了性能牺牲安全性。如果某个白名单规则导致性能问题,优先考虑调整 Agent 的实现方式(比如批量读取而不是逐个读取),而不是放宽权限。

5.4 与容器方案的对比:什么时候该用哪个

OpenShell 和容器不是互斥的,而是互补的。下面这个表可以帮助你判断什么时候用哪个:

维度OpenShell容器(Docker)虚拟机
启动开销微秒级秒级十秒级
隔离强度中高最高
文件系统交互直接,路径级控制需要挂载,卷级控制需要共享文件夹
网络控制系统调用级网络命名空间级虚拟网卡级
适用场景本地开发、短任务、高频调用生产部署、多租户完全不可信代码
配置复杂度中低高

我的建议是:开发阶段用 OpenShell,快速迭代,权限调整方便;生产环境如果 Agent 面对的是不可信输入,在 OpenShell 外面再套一层容器;如果 Agent 要执行完全不可信的代码(比如用户提交的脚本),直接用虚拟机或者专门的代码沙箱服务。

6. 几个容易被忽略的细节

6.1 环境变量清理:别让 Agent 继承不该有的信息

OpenShell 默认会清空 Agent 的环境变量,只保留显式设置的。这个设计很重要,因为环境变量里可能包含敏感信息,比如 API key、数据库连接串、内部服务地址。如果 Agent 被控制,这些信息可能被泄露。

但清空环境变量也可能导致 Agent 无法正常工作。比如某些工具依赖HOME、USER、TMPDIR这些变量。你需要在配置里显式设置它们。

[environment] clear = true set = { PATH = "/usr/bin:/bin", HOME = "/tmp/agent-home", TMPDIR = "/tmp/agent-tmp", LANG = "C.UTF-8" }

实操心得:HOME和TMPDIR建议指向沙箱内的临时目录,不要指向真实用户目录。这样即使 Agent 在 HOME 下写文件,也不会污染真实环境。OpenShell 可以配合tmpfs挂载,让这些目录在 Agent 退出后自动清理。

6.2 信号处理:Agent 被终止时的清理逻辑

当 Agent 任务超时或者被手动终止时,OpenShell 会发送 SIGTERM 信号。Agent 需要正确处理这个信号,做必要的清理工作(关闭文件、删除临时文件、提交或回滚事务)。

但这里有一个坑:如果 Agent 在信号处理函数里做了危险操作(比如删除文件),而这个操作不在白名单里,会被拦截。这可能导致清理逻辑失败,留下垃圾文件。

建议的做法是:把清理逻辑放在 OpenShell 的 supervisor 层面,而不是 Agent 内部。OpenShell 可以在 Agent 退出后自动清理指定的临时目录,不依赖 Agent 自己处理。

[cleanup] on_exit = ["/tmp/agent-tmp/**", "/tmp/agent-home/**"]

6.3 日志与审计:出了事怎么查

OpenShell 会记录所有被拦截的操作。这些日志对于排查问题和审计非常重要。建议把日志输出到独立文件,并且设置合理的轮转策略。

[logging] level = "info" file = "/var/log/openshell/agent-audit.log" format = "json" max_size_mb = 100 max_files = 10

日志里会包含时间戳、Agent ID、系统调用类型、路径、决策结果(允许/拒绝)、拒绝原因。如果发现 Agent 频繁尝试被拒绝的操作,可能说明能力清单配置有问题,或者 Agent 的行为偏离了预期,需要进一步调查。

注意:审计日志本身也可能包含敏感信息(比如 Agent 尝试访问的路径)。确保日志文件的权限设置正确,只有授权人员可以读取。

6.4 版本升级与策略兼容性

OpenShell 还在活跃开发中,版本升级可能带来配置格式的变化。建议在升级前先看 changelog,确认能力清单格式是否有 breaking change。另外,建议把能力清单纳入版本控制,每次修改都有记录,方便回滚。

如果升级后发现 Agent 行为异常,首先检查是不是默认策略变严格了。新版本可能默认拦截了之前放行的某些系统调用。这种情况下,要么调整配置显式放行,要么调整 Agent 实现避免使用被拦截的调用。

我在实际使用中的体会是,OpenShell 的隔离思路代表了一种趋势:AI Agent 的能力越强,运行时的约束就越重要。与其在 prompt 层面反复叮嘱“不要做危险操作”,不如在系统层面直接锁死权限。前者依赖模型的“自觉”,后者依赖工程的“强制”。对于任何要认真落地的 Agent 项目,后者才是可靠的基础。