Documentation
¶
Overview ¶
Package audit 是审计插件:把每次 MCP 调用的完整上下文交给使用方注册的落地函数。 基座不参与落地方式,日志、消息队列、数仓都由使用方决定。
安装位置:应最先安装,位于**插件链**最外层——既能记录被内层插件(quota / confirm) 拒绝的调用,也能看到内层插件(如 spill)改写后的最终结果。
但它不是整条链的最外层:**基座的 token 准入固定装在所有插件之前**,见第 6 条。
使用方必须知道的七条:
- **投递是异步的、尽力而为的,永远不阻塞调用返回。** 中间件在返回路径上只把事件 塞进有界队列,落地由后台单 worker 做(见 async.go)。刻意**没有**「阻塞直到落地」 或「没审计就不返回结果」这类档位:审计判定发生在返回路径上、工具此刻已经执行完, fail-closed 挡不住任何副作用,只是把结果藏起来、还得劝调用方不要重试;真正需要 fail-closed 的是执行**前**的门(人白名单、二次确认、配额)。 代价是「每次调用必有记录」降级为尽力而为:队列打满、进程被 kill -9、机器掉盘 都会丢事件。因此丢弃绝不静默——计数 + 限流告警 + ReadStats() 供宿主接监控。
- 落地函数要在 Install 之前注册:一个都没注册时启动直接失败。 「审计插件在场但一条也没落」是静默失效,而 required_plugins 只校验插件在场。 接流之后再把 Sink 清空同样会被计成失败并告警(worker 侧兜底)。
- 每个 MCP 方法都会产生事件,包括 initialize、ping 与各类 notifications。 远端 Sink 请按 Event.Method 自行过滤。落地慢不再影响调用方,但会挤占队列—— 过滤放在 Sink 里,队列容量按 queue_size 调。
- 事件里没有凭据。Call.Headers 已由基座剔除 Authorization/Proxy-Authorization/ Cookie/Set-Cookie,本插件也拒绝把这些头名配进 headers(见 normalize),且不去读 *http.Request 绕过这层剔除。Subject.Token 按基座契约是 token 的**用途名**。
- 工具入参不得承载密文。Event.Args 是入参原文,`login(user, password)` 这类工具 的密文会原样进长期审计存储;基座不认识字段语义、代替不了你判断。确有需要时用 SetArgsRedactor 注册脱敏钩子(在截断之前执行)。这条同样适用于其它插件。
- **基座 token 准入的拒绝不进审计流**(终审实测,务必知道):那一层固定装在插件链 之前,`tools/call` 被它拒时直接返回,本插件根本不会被调用——越权尝试在审计流里 是 0 条事件、HTTP 状态还是 200。同理 `tools/list` 的按 token 过滤发生在本插件 记录**之后**,所以 Event.Result 里是过滤**前**的全量清单,不等于该 token 实际 看到的那份。两者目前的唯一信号是基座打的 `[mcp] warning: 准入拒绝 …` / `tools/list 过滤 …` 日志(限流,见 runtime/tokenauthz.go)。做「谁看见/试过哪些 工具」这类权限审计时,必须把那两条日志一并采集,不能只看审计流。
- Event.Meta 是 Call.Meta 的通用透传(拍平成字符串、键数与值长都有上限)。 它是外置插件把自己的上下文送进审计流的唯一通道——本插件不认识任何具体插件, 所以键名的含义由写入方负责,落地方按需过滤。
Event 是**完全自有的值拷贝**(labels 深拷、入参/结果已截断成 string、Meta 拍平成新 map),交给 worker goroutine 既没有 data race 也不会拖住大对象引用——异步化不需要 额外拷贝,但**新增字段时必须保持这条性质**(不要往 Event 里塞指针或共享 map)。
Index ¶
Constants ¶
const AnonymousActor = "(anonymous)"
AnonymousActor 是 Subject.ID 为空时 ActorID 返回的占位标识。
基座默认不信任客户端送来的身份头,因此 Subject.ID 常态为空串。落地方若直接把空串 当用户名写进审计,「匿名调用」与「某个 id 为空的人」就再也分不开了,所以这里给一个 一眼能看出不是人名的占位值,并另配 Event.HasIdentity 供程序判定。
const Name = "audit"
Name 是插件名,与 required_plugins 里的写法一致。
Variables ¶
This section is empty.
Functions ¶
func OnEvent ¶
func OnEvent(fn Sink)
OnEvent 注册一个审计落地函数,可注册多个(按注册顺序依次调用)。 必须在 Install 之前(或同时)调用,理由见包注释第 1 条。
func SetArgsRedactor ¶
func SetArgsRedactor(fn Redactor)
SetArgsRedactor 注册入参脱敏钩子(可选,传 nil 清除)。未注册时入参原样进事件。
Types ¶
type Config ¶
type Config struct {
Audit Section `toml:"audit"`
}
Config 对应配置文件的 audit 段。
刻意用具名字段结构体、且字段集与文档一致(基座的配置认领制要求):用 map 兜底解码 会让段内拼错的键被判为「已解码」,于是 `[audit] on_eror = ...` 被完全吞掉、插件按 默认值上线;少声明一个文档承诺的字段,则部署方照文档写全反而启动失败。
type Event ¶
type Event struct {
// At 是调用进入 audit 的时刻。
At time.Time
// LogID 与 HTTP 层回写的同名响应头一致,用于和宿主 access log 对账。
LogID string
// Method 是 MCP 方法名,tools/call、tools/list 之外还包括 initialize、ping、
// notifications/*,落地方可据此过滤。
Method string
Tool string // 非 tools/call 时为空
// Labels 是该工具的完整 labels(含基座投影的 name/pkg),进入时的快照。
Labels map[string]string
// Subject 是调用主体的**进入时快照**:audit 在最外层,返回路径上 Call.Subject
// (指针)已可能被内层插件改过,直接读会记成被改写后的身份。
Subject runtime.Subject
// HasIdentity 区分「有真实身份」与「匿名/未配置身份」,见 AnonymousActor。
HasIdentity bool
// Args 是入参 JSON(先脱敏、再按上限截断),ArgsTruncated 标记是否截断。
//
// **可能含非法 UTF-8 字节**:它是调用方送来的原始 JSON 字节,审计不替它改写
// (截断只剥掉尾部那个不完整字符,中段的坏字节原样保留)。落地前请按后端要求
// 自行转义或替换——Postgres text、部分 JSON 序列化器会对非法 UTF-8 直接报错,
// 那会让这条事件在你的 Sink 里落地失败(计入 ReadStats().Failed,不影响调用方)。
Args string
ArgsTruncated bool
// Result 是最终结果的摘要——**限于插件链之内**的最终结果。
//
// tools/list 是例外:基座的按 token 过滤在本插件记录之后才发生,所以这里是过滤
// **前**的全量清单,不是该 token 实际看到的那份(见包注释第 6 条)。
//
// tools/call:JSON **片段**——截断后可能不完整(例如 `[{"type":"text",`),
// 只供人读与全文检索,**不要 Unmarshal**。任意工具的结果形状不可控,
// 想保证合法 JSON 就只能整份留下,那正是截断要避免的事。
// tools/list:可见工具名列表,按**个数**裁剪,始终是合法 JSON。
Result string
ResultTruncated bool
// ResultErr 记录结果序列化失败的原因(不静默丢字段)。
ResultErr string
Cost time.Duration
// IsError 为真表示结果是错误态(业务级拒绝或工具报错)。
IsError bool
// Err 是链上抛出的协议级错误文案,空表示没有。
Err string
// DeniedBy / DenyReason 来自 Call.Meta,空表示未被任何插件拒绝。
DeniedBy string
DenyReason string
// Headers 是按配置采集的请求头,缺失的头值为 "-"。
Headers map[string]string
// Meta 是 Call.Meta 在链返回后的快照:任何插件写进 Meta 的内容都会到这里。
//
// 刻意做成通用透传而不是逐个插件开字段:audit 不认识 confirm、quota 这些插件,
// 加特化字段等于把插件语义搬进 audit,外置插件就再也用不上这条路。
//
// 值一律拍平成字符串(按 fmt.Sprintf("%v"))——审计后端要能直接写库,
// 任意嵌套结构会让落地方在序列化时才爆。单个值按 maxMetaValueBytes 截断;
// 键数超过 maxMetaKeys 时按键名排序取前 N 个(排序保证同一次调用在不同进程里
// 截出同一批键,否则对账时会各说各话)。
Meta map[string]string
}
Event 是一次调用的审计事件。字段在进入链时快照,返回后补齐结果类字段。
type Redactor ¶
Redactor 是入参脱敏钩子:入参进事件之前先过它一遍。
契约:返回改写后的切片,返回 nil 视为空入参。 传入的 args 是**本插件拷贝出来的副本**,钩子就地涂改也伤不到真实请求字节—— 这层防御不是可省的:Call.Args 与 MCP SDK 的 CallToolParamsRaw.Arguments 共享同一 底层数组,而工具入参的反序列化发生在链终点(钩子之后),一个写错的钩子否则会让 线上工具收到被涂改的参数。脱敏本就是少数部署才开的开关,这份拷贝不影响默认路径。
type Section ¶
type Section struct {
// Headers 是需要采集进事件的请求头名;不含凭据类头(配了即启动失败)。
Headers []string `toml:"headers"`
// QueueSize 是异步事件队列的容量(条),省略或填 0 取 4096;不能为负。
//
// 刻意**没有**「无界队列」的写法:落地方卡住时无界队列就是一条通往 OOM 的路,
// 而丢事件至少是有计数、有告警、能被监控看见的。
QueueSize int `toml:"queue_size"`
// FlushTimeout 是进程退出时排空队列的时间预算(duration 串,如 "3s"),
// 省略取 3s。它直接加在服务停止耗时上,别配太长。
FlushTimeout string `toml:"flush_timeout"`
// MaxArgsBytes 是入参进事件前的截断上限(字节),省略或填 0 取 1024;无法关闭。
MaxArgsBytes int `toml:"max_args_bytes"`
// MaxResultBytes 是结果进事件前的截断上限(字节),省略或填 0 取 2048;无法关闭。
MaxResultBytes int `toml:"max_result_bytes"`
// contains filtered or unexported fields
}
Section 是 audit 段的字段集,即本插件的对外配置契约。
type Sink ¶
Sink 是使用方注册的审计落地函数。
返回 error 而非无返回值:落地方是唯一知道「这条有没有写成功」的一方。 投递是异步的,返回 error 不会影响任何调用结果,但它会被计入 ReadStats().Failed 并触发限流告警——不返回错误,落地失败就只有落地方自己知道。
type Stats ¶
type Stats struct {
// Enqueued 是成功入队的事件数。
Enqueued int64
// Dropped 是被丢弃的事件数:入队时队列已满,或进程已进入停止流程。
Dropped int64
// Delivered 是已交给全部 Sink 且无一报错的事件数。
Delivered int64
// Failed 是至少有一个 Sink 报错或 panic 的事件数。
Failed int64
}
Stats 是审计投递的运行期计数,供宿主接监控用。
有了它,「异步会丢事件」才是一件可观测的事而不是一句免责声明: Dropped 持续增长说明队列容量或落地速度不够,Failed 增长说明 Sink 本身有问题。