文章指出“只允许 SELECT”的提示词不构成安全边界,并给出文件白名单、语法解析、资源配额、超时及原子输出等执行层约束。其核心是把 Agent 请求转换为即使输入错误也不会越权的进程契约。
假设有一个 AI Agent,它只有一个任务:读取 package.json,然后返回包名和版本号。
你可以在 prompt 里写上“只能使用 SELECT”。但如果 SQL 引擎仍然可以打开其他文件、加载扩展、无限运行,或者覆盖输出路径,那么这个 prompt 并没有建立安全边界。它只是描述了一个安全边界。
我在构建 sqrail 时遇到了这种区别。sqrail 是一个基于 DuckDB 的小型执行器,用于对显式指定的文件运行分析型 SQL。真正有价值的经验,不是如何生成更好的 SQL,而是如何把 Agent 的请求转化为一份进程契约:即使请求本身有误,这份契约仍然成立。
第二列才是强制约束真正开始发挥作用的地方。
Agent 引用的是逻辑表名,而具体路径由宿主程序提供:
sqrail run -t pkg=package.json --timeout 5s "SELECT name, version FROM pkg"
在 sqrail 0.3.4 上,它生成了以下结果:
{"name":"sqrail-site","version":"0.3.4"}
执行器会对每个绑定路径进行规范化。Globs 和分区 Parquet 目录会被展开、排序、去重,并且结果不能为空。绑定完成后,只有这些精确匹配的文件会被加入 allowlist,同时 DuckDB 的外部访问能力会被禁用。
接着,我尝试绕过绑定,直接读取同一个文件:
SELECT * FROM read_json_auto('package.json')
v0.3.4 的 Windows 版本拒绝了该请求,退出码为 4,并返回了 QUERY_FAILED 诊断信息,其消息以 Permission Error: Cannot access file 开头。
这个通用错误码值得注意:真正重要的保证是访问被拒绝,而不是出现一条专门标注为“sandbox”的错误消息。
仅仅检查字符串是否以 SELECT 开头是不够的。注释、多条语句以及其他语法结构,都会让基于文本前缀的规则变得很脆弱。
v0.3 的契约只接受一条经过解析且类型为 SELECT 的语句。其中也包括 VALUES,以及以 WITH 开头的查询。它会拒绝 DDL、DML、COPY、ATTACH、INSTALL、LOAD、PRAGMA 语句和多条语句。在配置被锁定之前,扩展自动加载、自动安装和社区扩展都会被禁用。
SELECT 1; SELECT 2
该语句返回退出码 4,并报告 MULTIPLE_STATEMENTS。
这仍然不是一个能够防御恶意 SQL 引擎的通用 sandbox。它是一份有意收窄范围的契约:只允许针对调用方显式绑定的文件,执行一条只读的分析语句。
Agent 在编写查询之前,往往并不知道 schema。仅仅为了获知列名就执行一次猜测出来的查询,既浪费预算,又把规划与副作用混在了一起。
sqrail 将整个流程拆分为三个命令:
schema -> inspect names and types
check -> bind and plan without running the query
run -> execute once
对于查询 package 信息的示例,check 报告了两个 VARCHAR 类型的结果列、一个解析后的输入文件,以及一份包含 READ_JSON_AUTO 的物理执行计划。编排器可以在消耗执行预算之前检查这份 JSON。
单独设置内存参数,并不能限制整个 Agent 任务。发现数据、推断 schema、制定计划、执行查询、将数据 spill 到临时存储、结果物化以及最终输出,都会消耗资源。
v0.3 执行器可以限制线程数、deadline、结果行数、输出字节数、输入文件数量、SQL 字节数、内存和临时 spill。它的超时计时从命令处理开始,而不是等到 DuckDB 真正开始执行时才启动。
我针对 Windows x86-64 版本重新运行了两个失败场景的探测:
DuckDB 的内存设置并不是操作系统级别的硬性 RSS 限制。如果威胁模型要求硬隔离,宿主程序仍然需要使用独立进程、容器、Windows job object,或其他等效的操作系统边界。
把 JSONL 流式写入 stdout 很有用,但它无法回滚。如果在输出若干行之后发生故障,消费者可能会收到一个格式有效、但内容不完整的前缀。
文件输出需要采用不同的语义。使用 -o 时,sqrail 会在目标文件所在目录中写入一个私有临时文件,只有结果全部生成完毕后,才会将其公开。POSIX 使用不可替换目标的硬链接提交;Windows 则使用不可替换目标且启用 write-through 的移动操作。
当我让 v0.3.4 写入一个已经存在的 .jsonl 目标文件时,它返回退出码 5,并报告 OUTPUT_EXISTS;原目标文件没有被替换。
这种区别应该明确写进工具契约:
当可以接受部分流式结果时,使用 stdout;
当消费者需要全有或全无的输出时,使用文件目标路径。
在向 Agent 提供本地 SQL 执行器之前,我会问:
所有可读取的文件是否都经过显式绑定和规范化?
语句类型是否由解析器检查?
扩展加载以及其他逃逸通道是否已被禁用?
发现、规划、执行、spill 和输出是否共用一套有界资源约束?
失败的输出是否可能变得可见,或者覆盖已有文件?
失败信息是否足够稳定,能让软件直接处理,而不必从自然语言文本中抓取信息?
哪些保证仍然需要操作系统级隔离?
prompt 可以要求模型谨慎行事。执行契约决定了模型不谨慎时会发生什么。
如果你向 Agent 开放了一个数据工具,在你的实践中,最先失守的是哪一道边界:输入访问、资源使用,还是输出提交?
完整的 v0.3 进程契约已公开在 CONTRACT.md 中,测量规则则位于 BENCHMARKS.md。
披露:我使用了 AI assistant 来整理公开规范并编辑本文。2026 年 8 月 7 日,在发布版 sqrail 0.3.4 Windows x86-64 压缩包的 SHA-256 摘要与版本校验和匹配之后,我重新运行了上述命令和失败场景。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。