Documentation
¶
Overview ¶
Package spill 是大结果落盘插件:超过阈值的工具返回值写进本地文件,返回值改写为 摘要 + 可下载的 spill id/URL,避免把几十兆内容塞进模型上下文。
安装位置:应**最后**安装,位于洋葱最内层——返回时第一个被唤醒,拿到的是未经改写的 原始结果,才能按真实大小判断是否落盘。装在 audit 之外会让审计记到大结果原文。
使用方必须知道的七条:
- 判定按**序列化后(wire)总字节**算,不是 payload 字节:JSON 转义会让文本膨胀、 base64 会让二进制膨胀 4/3,按 payload 判定是**低估**(fail-open)。估算走零分配 的长度计算,覆盖 TextContent 之外的 ImageContent / AudioContent / EmbeddedResource / ResourceLink 与 structuredContent;无法序列化的值按「超大」 处理。只看文本的实现挡不住一张 8MB base64 图片。
- 错误态结果(IsError)**同样落盘**:一条几十兆的错误堆栈对上下文的伤害和成功结果 没有区别。改写后的结果保持 IsError 不变。
- 落盘失败绝不静默。默认 on_error = "deny"(fail-closed):宁可让这次调用失败, 也不把超过阈值的结果原样返回——那正是本插件要防的事。显式配成 "warn" 才降级为 「保留原结果 + 打告警日志」,此时上下文会被大结果撑一次。
- 磁盘有硬上限:max_file_mib(单文件)与 max_total_mib(目录总量),单位是 MiB。超单文件 上限直接失败(走 on_error),超总量上限先按最旧优先淘汰,腾不出空间才失败。 落盘文件按 ttl 到期回收(gc_interval 为扫描周期),进程退出时只停回收协程、 不删文件(正在下载的结果不该因一次重启消失),残留文件由下次启动的首轮扫描清掉。 回收只动「本进程创建的 / id 属主是本副本的 / 无属主信息且已超 ttl+24h 的」文件, 多个部署共享同一 dir 时不会互删;仍建议每个部署用独立 dir。
- 下载端点 /spill/<id> 由基座套 token 鉴权(Registry.Route),并在此之上做**属主 校验**:落盘时记下 Subject.Token(用途名)与有身份时的 Subject.ID,非属主一律 404(不是 403,也不回显属主地址——避免存在性与拓扑泄漏)。
- 多副本部署:id 内嵌产出该文件的副本地址,请求打到别的副本时,若该地址在 peer 白名单内则反向代理到属主副本(带环路保护头,Authorization 原样透传),否则 404。 摘要里的绝对 URL 依赖 PublicBaseURL,它必须是**本副本可直连的地址**,配成负载 均衡入口会让下载随机落到非属主副本。 **已知风险(复审 M-3,与基座 owner_routing 的既有约定一致,本插件不单独改)**: 副本间转发走明文 http://,且把调用方的 Bearer token 原样带给属主副本 —— 内网抓包即得该 token。副本跨机部署时请给 peer 之间加 TLS,或改用副本间的 内部凭据替代透传调用方 token。
- 落盘内容是工具结果原文,会以文件形式留在磁盘上 ttl 那么久。工具返回值里若有 敏感数据,落盘目录的权限(启动时收紧为 0700,目录不属于本进程或收紧失败则拒绝 启动)、属主校验与 ttl 就是它的全部保护。目录在校验通过后被以句柄形式持有 (os.OpenRoot),启动之后把路径换成软链既改不了写入位置、也读不出别处的文件。
Index ¶
- Constants
- Variables
- func CreatePath(f Format) (id, path string, err error)
- func Install(r *runtime.Registry) error
- func Put(name string, f Format, data []byte) (id string, err error)
- func PutFor(sub *runtime.Subject, name string, f Format, data []byte) (id string, err error)
- func SetDefaultDir(dir string)
- func URLFor(id string) string
- type Config
- type Format
- type Info
- type Section
- type Writer
Constants ¶
const ( // OnErrorDeny 为默认值:落盘失败即返回错误,不把超大结果交给模型。 OnErrorDeny = "deny" // OnErrorWarn 保留原结果,但必然打出告警日志(不存在「静默」这一档)。 OnErrorWarn = "warn" )
落盘失败时的处置策略(对应 spill on_error)。
const MetaID = "spill.id"
MetaID 是本插件写进 Call.Meta 的键:本次调用落盘后的 spill id(未落盘时不写)。
关于「跨插件 key 该由谁声明」的澄清(基座 runtime/call.go 里的纪律要求跨插件读取的 key 在基座声明,而核心不允许 import 插件,两者看似冲突):那条纪律管的是**基座自己 参与的** key(如 DenyResult 写的 denied_by / deny_reason,基座写、插件读,必须由基座 声明常量)。插件自己产生的 key 属于插件私有命名空间,由插件导出常量、并按 「插件名.」前缀避免撞名;读取方是插件或宿主,它们**可以** import 本插件拿到这个常量 (只有核心不允许 import 插件)。不愿意 import 的读取方按字符串约定 "spill.id" 读。
const Name = "spill"
Name 是插件名,与 required_plugins 里的写法一致。
Variables ¶
var ErrNotInstalled = errors.New("spill 插件尚未安装(或已停止):请先 spill.Install(r)")
ErrNotInstalled 表示还没有 Install(或已经 OnStop)就调用了写入/读取 API。
刻意不「按需自动初始化一个默认 store」:那会在没人配置落盘目录时悄悄往系统临时 目录写业务数据,且与配置认领制给出的目录不是同一个——宿主会看到 spill_explore 找不到自己刚写的文件。
Functions ¶
func CreatePath ¶
CreatePath 在 spill 存储里预留一份内容,返回 id 与可直接写入的**文件路径**。
给路径而不是 io.Writer,是因为宿主侧真正的写入方常常是只接受文件名的第三方 (批量执行框架、日志库)。它们会自己创建/截断这个路径,所以这类内容里没有本 插件的文件头,格式记在文件名后缀里。
与 Create 的差别(选用判据):
- Create 走本插件的 Writer:有单文件上限、内容可绑定属主(CreateFor)。
- CreatePath 交出路径后本插件管不到写入过程:**没有**单文件上限(越界要靠 调用方自己或 ulimit 兜底),内容按共享处理(任何通过 token 认证的调用方 都能下载,见 Put 的说明)。
写入方顺带产生的兄弟文件(<path>.wf 之类)与本份内容共用同一个 id,会一起被 TTL 回收;但只有 path 这一个文件能被下载与 spill_explore 探索。
func Put ¶
Put 把一段宿主产生的内容写入 spill 存储,返回可用于下载与探索的 id。
典型用法是「工具自己产出了一份大结果,不想经中间件的自动落盘」:例如把一次 SQL 查询结果导成 CSV、把批量接口的返回摊平成 jsonl 交给 agent 逐行探索。
属主:写入方不是一次 MCP 调用,没有可比对的调用主体,因此这类内容是**共享**的 ——任何通过 `/spill/<id>` token 认证的调用方都能下载(见 Info.Shared)。 只该给某一个人看的内容不要用它写;需要按人隔离时用 PutFor 传入 Subject。
func PutFor ¶
PutFor 与 Put 相同,但把内容绑定到某个调用主体:只有同一个 token 用途名 (主体有身份时还要求同一个人)才能下载,与中间件自动落盘的属主判定一致。
在工具处理函数里能拿到 *runtime.Call 时优先用它——那时「这份内容是谁的」是确定的。
func SetDefaultDir ¶
func SetDefaultDir(dir string)
SetDefaultDir 设置「配置里没写 dir 时」使用的落盘目录,必须在 Install 之前调用。
为什么需要它:落盘目录常常只有**运行期**才知道——GDP 之类的框架把它算成 `<应用根目录>/data/spill`,容器里又可能是挂进来的卷。静态配置文件写不出这个值, 而缺了它就会退回系统临时目录:那是可预测路径、重启即清,工具结果原文不该落在那。
配置里显式写的 dir 优先:部署方在配置文件里写下的东西必须赢过代码里的默认值, 否则「我明明配了却不生效」。传空串清除。
Types ¶
type Config ¶
type Config struct {
Spill Section `toml:"spill"`
}
Config 对应配置文件的 spill 段。
用具名字段结构体、且字段集与文档一致(基座的配置认领制要求):用 map 兜底解码会让 段内拼错的键被判为「已解码」,`[spill] threshhold_bytes = ...` 于是被完全吞掉、插件按默认 阈值上线;少声明一个文档承诺的字段,则部署方照文档写全反而启动失败。
type Format ¶
type Format string
Format 是宿主写入内容的格式声明。它决定下载时的 Content-Type,也决定 spill_explore 能对它做哪些操作(jsonl 可逐行 jq、text 只能 read/grep)。
type Info ¶
type Info struct {
// ID 是内容 id(内嵌产出副本的地址,见 runtime.NewOwnedID)。
ID string
// Name 是宿主写入时给的名字,仅用于人读与下载文件名。
Name string
// Format 是写入时声明的格式。
Format Format
// Size 是 payload 字节数(不含内部头部)。
Size int64
// ModTime 是最后一次写入时间;TTL 从它开始算。
ModTime time.Time
Shared bool
}
Info 是一份 spill 内容的元信息。
type Section ¶
type Section struct {
// Dir 是落盘目录;省略则用 <系统临时目录>/mcp-toolify/spill。
Dir string `toml:"dir"`
// ThresholdBytes 是落盘阈值(字节):结果总字节超过它即落盘。省略或 0 取 64KiB。
ThresholdBytes int `toml:"threshold_bytes"`
// TTL 是落盘文件的保留时长(duration 字符串);省略取 30m。
TTL string `toml:"ttl"`
// GCInterval 是回收扫描周期(duration 字符串);省略取 5m。
GCInterval string `toml:"gc_interval"`
// PreviewBytes 是摘要里保留的预览字节数;省略或 0 取 2048。
PreviewBytes int `toml:"preview_bytes"`
// MaxFileMiB 是单个 spill 文件的上限(**MiB**);省略或 0 取 64。
MaxFileMiB int64 `toml:"max_file_mib"`
// MaxTotalMiB 是落盘目录的总量上限(**MiB**);省略或 0 取 512。
MaxTotalMiB int64 `toml:"max_total_mib"`
// OnError 是落盘失败时的处置策略:deny(默认,fail-closed)| warn。
OnError string `toml:"on_error"`
}
Section 是 spill 段的字段集,即本插件的对外配置契约。
字段名自带单位,因为这些数字的量级差得很远:阈值/预览是「一份结果多大」(KiB 级, 用字节),磁盘上限是「这个部署最多占多少盘」(MiB 级,用 MiB)。写成统一的裸字节 会让 67108864 这类值必须心算,改的时候还容易多打或少打一个 0。
ttl / gc_interval 是**字符串**("30m"、"2h"):TOML 没有 duration 类型, 而 time.Duration 是 int64,写 ttl = "30m" 会直接解码失败、写 ttl = 1800 又会被 当成 1800 纳秒。字符串 + time.ParseDuration 是唯一不会被误读的写法。
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer 是一份正在写入的 spill 内容。
func Create ¶
Create 创建一份可**增量写入**的 spill 内容,返回的 Writer 边写边可被 spill_explore 读到(status=running 时就能读,不必等写完)。
这是异步任务(批量探测、长时间导出)该用的入口:先拿到 id 交给调用方, 后台协程持续追加。Writer 不是并发安全的,多个协程写同一份内容请自行串行化。