安全模型
Wippy 的安全模型定义了你的代码可以访问什么、不能访问什么,以及由谁来强制执行这些边界。在构建之前值得先读一读,因为它工作在大多数框架合并为一层的两个层次上:运行时隔离每个进程,使危险能力干脆不存在;而基于属性的策略层管理进程被允许使用哪些注册表能力。理解这两者会改变你构建应用的方式。
信任模型
Wippy 的隔离层不给进程任何环境权限。一个全新的 Lua 或 WASM 进程无法触及文件系统、网络、宿主操作系统或其他进程的内存,因为这些能力不存在于它的环境中。能力只通过注册表到达:进程被明确授予的函数、工具、连接和配置。
在此之上,对注册表能力的访问由基于属性的访问控制(ABAC)管理。每个受保护的操作都会对照当前 actor 的安全 scope 进行检查,scope 是一组允许或拒绝对某资源执行某动作的策略,可选地以 actor 和资源元数据为条件。这是声明式的:你在配置中定义策略,而不是在应用代码中。
当进程同时带有 actor 和 scope 运行时,访问是默认拒绝的:只有在某条策略明确许可且没有任何策略拒绝时,请求才被允许。严格模式管理不完整的情况,即没有建立 actor 或 scope 时。它默认开启,因此不完整的上下文会被拒绝;在运行时配置中设置 security.strict_mode: false 则改为选择宽松行为。需要提前规划的后果是:在默认设置下,没有声明安全上下文的进程会在每次检查中失败——给这样的进程在其条目上加一个 security: 块,或者通过提供安全上下文的路径来启动它。结合最小权限策略,这在“因缺失而拒绝”的隔离之上为你提供了失败即关闭的授权。策略语法、求值规则以及 security: 块的结构参见安全参考。
进程隔离
Wippy 中的每个执行单元都运行在一个隔离进程中,带有自己的嵌入式解释器(Lua 或 WASM)。
进程拥有什么: 自己的内存空间(Lua 的基线开销约 13 KB)。注册表的一个受限视图。一个 actor 身份和一个安全 scope。一个带崩溃恢复和重启限制的受监管生命周期。
进程没有什么: 对文件系统的访问(除非通过注册表控制的文件系统条目)。对网络的访问(除非通过被授予的 HTTP 客户端或工具模块)。对其他进程内存的访问。对承载它的 Go 运行时的访问。对环境变量的访问(除非通过被授予的 environment 条目)。
隔离如何强制执行: 每个 Lua 进程从一个最小标准库启动。文件 I/O、操作系统进程访问、动态代码加载和网络从不被加载,因此它们不存在于环境中,进程也无法恢复不存在的东西。模块加载受到限制:require 只解析进程被明确授予的模块和注册表条目,没有文件系统搜索路径。WASM 进程通过 WASI 达到等效的隔离:只有为该条目配置的宿主函数和挂载的文件系统条目是可达的。
这不是通过运行时权限(如 seccomp 或 AppArmor)实现的沙箱化。这是通过缺失实现的沙箱化。危险能力从不被加载,因此它们无法被利用、绕过或提权。
能力控制
注册表是 Wippy 的能力存储,安全策略是它的授权层。
每项能力都是一个注册表条目。 函数、工具、智能体定义、数据库连接、环境引用、配置值和定时任务全都是带有声明 kind、schema 和元数据的注册表条目。条目在注册时由其 kind 处理器验证。
条目 ID 是带命名空间的。 ID 的形式为 namespace:name,带一个冒号,命名空间通过点分段实现层级化,例如 tenant_acme.tools:read(命名空间 tenant_acme.tools,名称 read)。策略匹配动作和资源,资源模式可以针对某个命名空间前缀,因此单条规则就能覆盖整个命名空间。
策略决定访问。 每次能力访问(注册表查找、函数调用、数据库句柄、文件打开)都会对照 actor 的 scope 进行检查。策略声明它覆盖的动作和资源、允许或拒绝的效果,以及对 actor 和资源元数据的可选条件。求值发生在每次访问时,而不是在启动时一次性完成:如果任何策略拒绝,访问被拒绝;如果至少有一条允许且没有任何一条拒绝,则被允许;如果没有策略匹配,访问被拒绝。(当上下文完全没有 actor 或 scope 时,这种不完整的情况由严格模式而非策略求值来决定。)
上下文是声明出来的,不是凭空继承的。 函数继承调用者的 actor 和 scope。派生出的进程同样继承它们:其帧从派生方的帧分叉而来,然后其自身条目上的 security: 块修改这个继承来的上下文——该块指定的 actor 会替换继承的 actor,而它按注册表 ID 列出的策略和策略组会合并进继承的 scope。解析是原子的——如果任何指定的策略或策略组缺失,派生会失败,而不是带着不完整的 scope 继续。CLI 命令还可以额外声明 meta.command.security,仅在操作者亲自启动命令的受信任启动路径上生效。
工具参数由 schema 定形。 工具为其输入声明一个 JSON Schema。该 schema 会提供给模型,使其生成符合规范的参数,并且在调用运行之前会对工具的访问进行策略检查。
数据边界
数据库连接是注册表条目。 进程不会自己拼装连接字符串。它按注册表 ID 请求一个连接,该请求在返回句柄之前会经过策略检查。策略未授予租户 B 数据库条目的进程无法获得指向它的句柄。
LLM API 密钥存放在环境系统中。 Claude、GPT 和其他提供商的密钥从环境系统读取(例如通过 env.storage.os 条目暴露的操作系统环境变量,由 env.variable 条目引用,其读取通过 env.get 动作进行策略检查)。提供商在内部读取它们;它们不会在进程参数中传递,也不会返回给调用代码。
文件和 blob 存储遵循相同的模型。 进程通过文件系统或云存储注册表条目进行读写,每次访问都经过策略检查。WASM 进程只能通过为该条目明确挂载的文件系统条目访问文件。
智能体安全
智能体是由 LLM 驱动、会使用工具的进程。它们在运行时做出你的代码并不直接控制的决策,所以它们的边界很重要。Wippy 通过与其他任何进程相同的注册表和策略机制来处理这一点。
工具访问。 智能体只能调用其定义中列出的工具,并且每次工具执行都通过 funcs.call 运行,该调用会经过策略检查。被拒绝的调用会在工具函数运行之前失败。一个被设计为读取客户数据但不能删除它的智能体,要么定义中没有删除工具,要么该动作被策略拒绝。
外部工具与 MCP 工具。 Wippy 可以消费外部工具,也可以通过 Model Context Protocol 暴露自己的工具。被消费的工具走与原生工具相同的函数调用路径和策略检查。Wippy 暴露给外部 MCP 客户端的工具由受限作用域、可撤销的访问令牌把关,这些令牌限制客户端可以执行哪些动作。
结构化输出。 LLM 模块可以使用提供商原生的结构化输出支持请求受 schema 约束的(结构化)输出,因此智能体的输出可以被约束为声明的形状。
可观测性。 启用 OpenTelemetry 后,LLM 提供商调用和工具调用会被追踪,token 用量通过 usage-tracker 契约记录。这为你提供了智能体调用了什么、花费了多少的审计轨迹。参见可观测性。
自我修改的边界。 被允许在某个命名空间中创建工具的智能体,可以被拒绝对另一个命名空间中它自己的定义的写权限。注册表写入是受策略检查的动作,因此对智能体自身命名空间的拒绝策略会阻止它编辑自身或给自己授予新的访问权限。
多租户强制执行
对于多个客户共享单个 Wippy 实例的部署,隔离是在任何操作运行之前由策略求值强制执行的,而不是由应用代码检查租户 ID 来实现。
租户隔离由策略强制执行。 给每个租户一个 actor 和一个 scope,其策略只覆盖该租户的命名空间。在严格模式开启的情况下,租户的进程会在其代码运行之前被拒绝访问其 scope 之外的资源。有效的隔离取决于编写这些按租户的策略;运行时强制执行它们,但不会替你推断租户归属。
跨租户访问是显式的。 跨租户共享的能力位于每个租户策略都允许的共享命名空间中。共享按命名空间选择加入。
并发在宿主层受限。 进程宿主通过工作池限制并发。进程组(pg.scope)提供隔离的、集群范围的成员关系和广播命名空间,并可以限制组和成员数量。按租户的 CPU 或内存上限不是内置的运行时特性;请在基础设施层强制执行。
一份专门的多租户架构指南正在规划中。
范围与限制
Wippy 的安全模型涵盖进程隔离、能力控制和数据边界。以下内容在运行时的范围之外,仍然由你的基础设施负责。
静态数据加密。 数据库、磁盘和 blob 存储加密由底层基础设施处理(PostgreSQL TDE、磁盘加密等)。Wippy 假定存储层负责加密。
网络级隔离。 进程隔离发生在应用层。Wippy 与其依赖(数据库、LLM API、外部服务)之间的网络分段由基础设施处理:VPC、安全组、防火墙。
身份管理。 认证(验证用户是谁)由你的认证层处理。Wippy 的安全模型从认证之后开始:它控制一个已认证用户的进程能做什么,而不是这个用户是谁。携带 actor 和 scope 的令牌可以通过令牌存储签发和验证。
基础设施审计日志。 Wippy 的追踪覆盖进程级操作:函数调用、工具调用、进程活动。基础设施级访问(SSH 到服务器、数据库管理操作)应由基础设施工具审计。
常见问题
一个租户的智能体能访问另一个租户的数据吗? 在每个租户的资源都由策略限定作用域时不能。有了按租户的策略和严格模式,运行时会在智能体代码运行之前拒绝对租户 scope 之外资源的访问。
智能体能提升自己的权限吗? 只有当它的策略允许写入自己的定义时才能。注册表写入受策略检查,因此对智能体自身命名空间的拒绝策略会阻止自我修改。能在某个命名空间中创建工具的智能体,无法给自己授予其 scope 尚未覆盖的命名空间的访问权限。
我怎么查看智能体做了什么? 启用 OpenTelemetry 后,LLM 和工具调用会被追踪,token 用量通过 usage-tracker 契约记录。参见可观测性。
如果智能体行为异常会发生什么? 它被沙箱所约束:没有文件系统,没有网络,没有操作系统,除了被授予的之外无法访问其他进程。它只能调用其定义中被策略许可的工具,并且这些调用都会被记录。
租户隔离是由我的代码还是由运行时强制执行的? 由运行时。策略引擎在操作运行之前对每次访问求值。你的工作是编写按租户的策略;运行时负责强制执行。
外部 MCP 工具如何保证安全? 通过 MCP 消费的工具走与原生工具相同的函数调用路径和策略检查。Wippy 暴露给外部 MCP 客户端的工具由受限作用域、可撤销的访问令牌把关。连接一个 MCP 服务并不会绕过安全模型。
安全参考
| 关注点 | Wippy 的做法 |
|---|---|
| 进程隔离 | 每个进程独立的解释器(Lua 或 WASM),无共享内存 |
| 默认访问 | 同时设置了 actor 和 scope 时,未匹配的策略拒绝访问;严格模式默认开启,在没有建立 actor 或 scope 时拒绝 |
| 上下文声明 | 条目上的 security: 块(actor、策略、策略组);解析是原子的且失败即关闭 |
| 供应链 | 模块包在安装时和启动时按摘要校验;不匹配则拒绝该模块 |
| 节点间信任 | 双向认证的节点间网格;每个节点一个 ed25519 身份,显式的受信任对等节点映射 |
| 工作流传播 | actor 和 scope 以签名的、受众绑定的头传递给 Temporal;验证失败则执行失败 |
| 能力控制 | 注册表条目由基于属性的安全策略管理(actor、scope、动作、资源) |
| 数据边界 | 连接和存储都是注册表条目;每次访问按条目 ID 进行策略检查 |
| API 密钥管理 | 存放在环境系统中,由提供商在内部读取,不暴露给进程代码 |
| 智能体工具控制 | 工具限于智能体的定义;每次调用通过 funcs.call 策略检查 |
| 外部工具(MCP) | 相同的函数调用路径和策略检查;暴露的工具由受限作用域令牌把关 |
| 智能体审计轨迹 | OpenTelemetry 追踪(启用时)加上 usage-tracker 记录 |
| 多租户隔离 | 按租户的策略和 scope 由运行时在每次操作之前求值 |
| 并发限制 | 由宿主工作池限制;不内置按租户的 CPU/内存上限 |
| 自我修改 | 对注册表写入动作的拒绝策略阻止智能体编辑自己的定义 |