数据安全设计文档¶
本文档面向接手该模块的团队成员,记录 AI Agent 操作数据时的安全架构决策,接口设计和实现指导.
关联文档: - architecture.md - 全局架构,ToolCapability/OperationLog/安全审计体系 - defensive-programming.md - 三层防御通用模式 - research-agent-database-safety.md - 数据库隔离技术调研 - research-operation-history.md - 操作历史/回滚方案调研
目录¶
- 核心原则
- 三个操作维度
- AI 与 DB 关系的架构决策
- 读操作防护
- 写操作分级
- 凭据安全:SetSecret 接口
- Dry-run 模式设计
- Staging 写入模式
- 审计日志设计
- 四层安全防御
- WMS 波次建立参考案例
- LLM 多轮推理分支场景
- 已推翻的旧结论
- 引擎接口清单
核心原则¶
AI 负责决策,业务系统 API 负责执行.AI 不直接写业务数据库.
这不是一条"最佳实践",而是边界划分.理由:
- AI 生成的 SQL 是自然语言到结构化语言的翻译,语义歧义不可消除.
- 业务系统的不变量(库存不能为负,金额精度,外键约束)最好由业务系统自己守护.
- 审计责任链要清晰:谁批准了,谁执行了,系统验证了什么.
引擎层的职责:提供 Dry-run 接口,写操作分级声明,审计钩子.
消费层的职责:实现具体的 DB 驱动,SQL 验证器,人工审批流程.
三个操作维度¶
Agent 操作数据的场景可以归纳为三类,每类有不同的安全机制:
| 维度 | 典型操作 | 引擎现有支持 | 本文补充 |
|---|---|---|---|
| 文件操作 | FileEdit,FileWrite | 原子写入 + 备份(FileHistory),OperationLog Saga 补偿 | - |
| 数据库操作 | SQL 查询,记录更新 | AuditSink 接口,ToolCapability.DryRunnable | 本文重点 |
| API 调用 | 第三方 REST,webhook | Reversible 接口,Saga 补偿 | Dry-run 验证流程 |
文件操作的安全机制已在 architecture.md#FileHistory 文件历史系统 和 architecture.md#OperationLog 统一操作日志 中详细记录,本文不再重复.
AI 与 DB 关系的架构决策¶
决策图¶
外部世界
│
│ Agent 永远通过这一层
▼
业务系统 API(WMS、OMS、ERP...)
│ ↑
│ │ Dry-run 时:事务内 diff 回传
│ │ 正式写时:短事务 + CAS 乐观锁
▼ │
生产数据库(只读账号 + staging 隔离)
│
▼
审计日志(异步,不阻塞主事务)
为什么 AI 不直接写¶
| 方案 | 问题 |
|---|---|
| AI 直接执行 INSERT/UPDATE | 无业务不变量校验,无人工审批窗口,难以追责 |
| AI 写 staging 表 | 可接受,但需要业务系统配合 |
| AI 调用业务 API(dry-run → 正式) | 推荐:API 层自带不变量校验,天然有两阶段窗口 |
读操作防护¶
即使是读,也需要防护.AI 可能因提示注入而执行恶意查询(全表扫描,高频轮询).
防护层次¶
基础设施层(硬约束,代码绕不过)
└── 只读数据库账号:GRANT SELECT ON schema.* TO ai_readonly
工具层(第二道防线)
└── SQL 解析器:非 SELECT 语句直接拒绝返回错误
规则:不允许 INSERT / UPDATE / DELETE / DROP / TRUNCATE / ALTER / CALL
查询层(第三道防线)
└── 行数限制:LIMIT 注入(若 AI 未写 LIMIT,自动追加 LIMIT 1000)
└── 查询超时:context.WithTimeout,超时后取消连接
└── 表名白名单:可配置,仅允许查询指定表
消费层实现要点¶
引擎提供 ToolCapability 接口声明,具体校验由消费层在工具实现中完成:
// 消费层示例:SQL 读工具(伪代码,消费层实现,不在引擎层)
type SQLReadTool struct {
db *sql.DB // 只读账号连接
maxRows int // 默认 1000
timeout time.Duration
tables []string // 白名单,空=允许所有
}
func (t *SQLReadTool) Execute(ctx context.Context, params map[string]any) (tools.Result, error) {
query := params["query"].(string)
// 1. SQL 解析:拒绝非 SELECT
if !isSelectOnly(query) {
return tools.ErrorResult("只允许 SELECT 查询"), nil
}
// 2. 行数限制注入
query = injectLimit(query, t.maxRows)
// 3. 超时控制
ctx, cancel := context.WithTimeout(ctx, t.timeout)
defer cancel()
// 4. 执行
return executeQuery(ctx, t.db, query)
}
写操作分级¶
不是所有写操作风险相同.分级的目的是让 AI 权限与操作风险严格匹配.
风险分级表¶
| 级别 | 操作类型 | 典型场景 | 处理方式 | 人工介入 |
|---|---|---|---|---|
| L1 | 追加写 | 审计日志,状态推进(pending→processing),任务记录新增 | 直接提交,异步审计 | 不需要 |
| L2 | 修改已有记录 | 更新订单状态,修改商品描述,数量调整 | 业务不变量检查后提交 | 按配置决定 |
| L3 | 高风险写 | 金额变更,库存核减,关键配置修改,跨系统同步 | 必须人工审批窗口 | 必须 |
| L4 | 不可逆删除 | 物理删除记录,清空分区,DDL 变更 | 默认禁止,需明确 override | 必须 |
分级声明方式¶
消费层工具通过实现 ToolCapability 接口声明自己的安全等级(引擎层已有此接口,详见 architecture.md#ToolCapability 安全协议):
// 引擎层已有接口(pkg/tools/tool.go),消费层工具实现
type CapabilityProvider interface {
Capabilities() ToolCapability
}
type ToolCapability struct {
Level int // 0=无保护 1=可回滚 2=DryRun+可回滚
RiskLevel string // "l1_append" / "l2_modify" / "l3_critical" / "l4_irreversible"
// ...
}
凭据安全:SetSecret 接口¶
核心问题¶
凭据(密码,Token,私钥)不能通过 prompt 传递.原因:
- Prompt 内容进入 LLM 上下文窗口,context 会被日志记录,快照序列化,压缩保留.
- 命令行参数会出现在
ps aux,/proc/<pid>/cmdline,shell 历史中,进程级可见. - 一旦凭据进入任何一层日志,脱敏极难做到完全彻底--漏一处就是全部泄漏.
设计决策¶
引擎提供 engine.SetSecret(name, plaintextValue) 接口,消费层在启动阶段注入凭据.Agent 在 prompt 中只使用凭据名称占位符引用,引擎在执行时将其替换为环境变量注入,凭据值永不出现在命令行参数中.
消费层启动阶段 Agent Prompt 引擎执行阶段
───────────────── ──────────────────── ────────────────────────────
engine.SetSecret( "用 ssh_pass 登录 export SSH_PASS=<实际值>
"ssh_pass", 目标主机..." ssh ... (环境变量注入)
"s3cr3t",
)
prompt 中引用: 审计日志记录:
{{ssh_pass}} [credential:ssh_pass]
(不记录实际值)
接口设计¶
// SDK 公开接口(pkg/engine/engine.go 或 pkg/engine/secrets.go)
// SetSecret 注册一个命名凭据。plaintextValue 仅存于内存,不序列化、不落盘。
// name 不区分大小写,推荐下划线格式(ssh_pass、db_password、api_token)。
func (e *Engine) SetSecret(name, plaintextValue string)
// 消费层示例:Agent 启动前注入
eng := engine.New(cfg)
eng.SetSecret("ssh_pass", os.Getenv("TARGET_SSH_PASSWORD"))
eng.SetSecret("db_password", os.Getenv("DB_PASSWORD"))
eng.SetSecret("api_token", vault.Get("my-service/api-token"))
Prompt 中的引用方式¶
Agent 在 prompt(system prompt 或 user message)中通过 {{name}} 占位符引用凭据名称:
引擎在工具执行前将占位符替换为对应的环境变量名(不是值),工具调用时以环境变量方式注入:
// 引擎执行 BashTool 时:
// cmd.Env = append(os.Environ(), "SSH_PASS=s3cr3t") // 注入凭据
// 命令本身:sshpass -e ssh user@host // -e 表示从环境变量读
// 命令行参数中不含密码明文
审计日志脱敏规则¶
凭据值出现在任何可观测路径(OperationLog,AuditEntry,Observer 事件,StderrObserver 输出)时,必须替换为 [credential:name] 标记:
// OperationLog 中的命令日志(BashTool 执行记录)
// 原始命令:sshpass -p s3cr3t ssh user@host
// 脱敏后: sshpass -p [credential:ssh_pass] ssh user@host
// AuditEntry.Extra 中
extra["command"] = "[credential:ssh_pass] 凭据已注入,命令已脱敏"
// Observer 事件中(StderrObserver / BufferedObserver)
// 任何包含凭据值的字符串,在写出前经过 engine.Redact() 替换
脱敏由引擎内部的 SecretStore 统一维护,所有写出路径在输出前调用 Redact(),消费层无需自行处理.
实现优先级与顺序¶
顺序不能颠倒:
- 先实现 SetSecret + 日志脱敏:凭据注入 +
engine.Redact()覆盖所有日志写出路径.没有脱敏之前,审计日志不能上传. - 再实现审计日志上传:脱敏确认完整后,才能将审计日志上传到外部 SIEM / 存储系统.若先上传后脱敏,泄漏窗口不可逆.
设计边界¶
| 项目 | 引擎层(本模块实现) | 消费层职责 |
|---|---|---|
| SetSecret 接口 | 提供,内存存储,不落盘 | 从 vault/env/配置文件读取实际凭据值后调用 |
| Redact() 脱敏 | 提供,覆盖所有引擎内部日志路径 | 消费层自定义日志组件调用 engine.Redact() |
| 环境变量注入 | BashTool 执行时自动注入 | 无需额外操作 |
| 凭据轮转 | 支持重复调用 SetSecret 覆盖旧值 | 消费层负责定期更新凭据 |
| 进程隔离 | 子进程环境变量隔离(不继承父进程全量 env) | - |
反向论证¶
反对意见:环境变量注入仍然可以通过 /proc/<pid>/environ 读取,并非完全安全.
回应:正确,但对比 prompt 传递,环境变量注入的攻击面更小:
- 需要相同 UID 或 root 权限才能读取 /proc/<pid>/environ
- 日志,上下文,快照这些更容易泄漏的路径被完全隔断
- 这是工程上的防御纵深,不是银弹--凭据安全的最终保障是权限收紧(最小权限原则),引擎只负责不主动扩大攻击面
Dry-run 模式设计¶
Dry-run 的核心思想:在不持久化副作用的情况下,看清楚"如果执行会发生什么".
数据库场景的 Dry-run(事务回滚模式)¶
业务 API 收到 dry_run=true 请求
│
▼
BEGIN TRANSACTION
│
▼
执行 AI 生成的 SQL(INSERT/UPDATE/DELETE)
│
▼
捕获 diff(before/after 快照,通过 RETURNING 子句或二次查询获取)
│
▼
ROLLBACK(不持久化)
│
▼
将 diff 序列化,返回给调用方
│
▼
ML 验证层(事务外,无锁期间执行,时间窗口不阻塞数据库)
│
├── 通过 ──► 正式执行请求(dry_run=false)
│ │
│ ▼
│ 短事务 + CAS 乐观锁(基于 version 字段)
│ │
│ ├── 乐观锁通过 ──► COMMIT
│ └── 乐观锁失败 ──► 重试(最多 3 次)或升级人工
│
└── 失败 ──► 返回拒绝原因,由 Agent 重新规划
关键设计决策¶
ML 验证在事务外执行(不是在事务内).
原因:ML 推理时间通常在 100ms ~ 2000ms,在事务内持有锁会导致: - 锁持有时间过长,影响并发写 - 数据库连接池耗尽 - 超时与锁冲突叠加
正确做法:事务仅用于读取 diff(毫秒级),拿到 diff 后立即 ROLLBACK,在事务外做 ML 验证.
乐观锁 CAS 保证执行时数据未被篡改.
Dry-run 到正式执行之间存在时间窗口,期间其他操作可能修改了目标记录.CAS 用 version 字段确保:
UPDATE orders
SET status = 'processing', version = version + 1
WHERE id = 123 AND version = 5 -- 执行时检查版本未变
若版本不匹配,说明数据在验证窗口内被修改,需要重新 Dry-run.
接口定义(消费层参考)¶
// 消费层工具实现 DryRunnable 接口(引擎层接口,消费层工具实现)
type DryRunResult struct {
SQL string // 实际执行的 SQL
AffectedRows int // 影响行数
Diff []RowDiff // 每行 before/after
Warnings []string // 非阻断性警告(如:影响 1000 行以上)
}
type RowDiff struct {
Table string
PK map[string]any
Before map[string]any
After map[string]any
}
边界条件¶
| 情况 | 处理方式 |
|---|---|
| SQL 语法错误 | Dry-run 直接返回解析错误,不开启事务 |
| 影响行数超过阈值(如 10000 行) | Dry-run 返回 Warning,强制要求人工确认 |
| 外键约束违反 | Dry-run 捕获数据库错误,返回给 Agent 重新规划 |
| Dry-run 成功但正式执行时乐观锁失败 | 最多重试 3 次,超过后升级 L3 需人工介入 |
| 数据库连接超时 | 统一返回 ErrDBTimeout,Agent 按超时处理,不重试写操作 |
Staging 写入模式¶
适用于"AI 要写,但生产系统没有 dry-run API"的场景(比如遗留系统,第三方服务).
流程¶
AI 生成写操作
│
▼
写入 staging 表(不触碰生产表)
e.g.: wave_candidates(待确认的波次方案),而非 wave(正式波次)
│
▼
自动规则检查(脚本/触发器)
├── 库存充足?
├── 业务约束满足?
└── 数量在合理范围内?
│
├── 自动通过(低风险追加写)──► 触发迁移任务 → 写入生产表
│
└── 需要人工确认(中高风险)──► 通过 permission.Handler 发起审批请求
审批通过 → 触发迁移任务 → 写入生产表
审批拒绝 → 清理 staging 记录 + 通知 Agent
与引擎的集成点¶
permission.Handler(pkg/permission/permission.go):审批请求推送给人工操作员security.AuditSink(pkg/security/audit.go):staging 写入和迁移执行都记录审计日志
Staging 表设计要点¶
-- staging 表比生产表多三列:
CREATE TABLE wave_candidates (
-- 业务字段与生产表相同...
-- staging 专有字段
agent_session_id VARCHAR(64) NOT NULL, -- 来自哪个 Agent 会话
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/approved/rejected/migrated
reviewer VARCHAR(64), -- 审批人(自动通过时为 'auto')
reviewed_at TIMESTAMP
);
审计日志设计¶
引擎已有 AuditSink 接口和 LocalAuditSink 实现(JSONL 追加写入).
本节补充数据库写操作场景的字段规范和使用要点.
所有 AI 写操作必须记录的字段¶
// 消费层填充 AuditEntry.Extra 字段(引擎已有 AuditEntry 结构)
extra := map[string]string{
// 必填
"session_id": session.ID, // 来自哪个 Agent 会话
"operation_id": uuid.New(), // 本次操作唯一 ID
// DB 写操作必填
"db_table": "orders",
"db_pk": "id=123",
"before_state": marshalJSON(before),
"after_state": marshalJSON(after),
// Dry-run 流程必填
"dry_run_passed": "true",
"ml_validator": "wave_validator_v2",
"ml_score": "0.97",
// 可选
"risk_level": "l2_modify",
"approver": "auto", // 或操作员 ID
}
审计日志写入时机¶
| 时机 | 必须记录 |
|---|---|
| Dry-run 执行(无论通过与否) | ✅ |
| ML 验证结果(通过/拒绝) | ✅ |
| 正式写入执行前 | ✅ |
| 正式写入执行后(含失败) | ✅ |
| 人工审批决定 | ✅ |
| 乐观锁冲突 + 重试 | ✅ |
异步写审计(不阻塞主事务)¶
审计日志通过 AuditObserver(EventObserver 实现)异步写入,主事务提交后发出事件,AuditObserver 在 goroutine 中处理,主路径零延迟.
权衡:异步意味着在极端情况(进程崩溃)下可能丢失最后几条日志.对于 L3/L4 高风险操作,消费层可选择同步写审计(在主事务 COMMIT 前 flush).
批量回滚(按 session_id)¶
当一个 Agent 会话的操作需要整体撤销时(如发现决策错误),通过 session_id 查询该会话所有写操作的 before_state,按时间倒序执行补偿:
SELECT * FROM audit_log
WHERE extra->>'session_id' = 'sess_abc123'
AND tool_name IN ('DBWrite', 'DBUpdate')
ORDER BY timestamp DESC;
-- 然后消费层逐条读取 before_state,执行补偿 SQL
四层安全防御¶
对于生产数据库写操作,四层防御全部在位才允许执行:
┌─────────────────────────────────────────────────────┐
│ 层 1:输出约束(SQL 白名单 + 业务规则预校验) │
│ AI 生成 SQL → 工具层解析 → 仅允许合法 SQL 语法 │
│ 业务规则:金额字段 > 0、数量字段为整数、时间格式合法等 │
└─────────────────────┬───────────────────────────────┘
│ 通过
┌─────────────────────▼───────────────────────────────┐
│ 层 2:Dry-run + ML 验证 │
│ 事务内执行 → 捕获 diff → ROLLBACK │
│ 事务外 ML 验证 → 语义合理性校验(异常 diff 拒绝) │
└─────────────────────┬───────────────────────────────┘
│ 通过
┌─────────────────────▼───────────────────────────────┐
│ 层 3:熔断器(连续失败自动暂停 AI 写权限) │
│ 5 次连续 ML 拒绝 → 熔断 15 分钟 │
│ 触发时通知操作员,人工 reset 才能恢复 │
└─────────────────────┬───────────────────────────────┘
│ 通过
┌─────────────────────▼───────────────────────────────┐
│ 层 4:人工 kill switch │
│ 操作员随时可通过 permission.Handler 暂停 AI 写权限 │
│ L3/L4 操作无论其他层是否通过,均需人工最终确认 │
└─────────────────────────────────────────────────────┘
熔断器与引擎的集成¶
熔断器逻辑属于消费层(不同场景的阈值不同),通过引擎的 permission.Handler 实现:
// 消费层:DB 写工具内部的熔断检查(伪代码)
func (t *DBWriteTool) Execute(ctx context.Context, params map[string]any) (tools.Result, error) {
if t.circuitBreaker.IsOpen() {
// 触发 permission.Handler,通知人工,返回等待
return t.permHandler.RequestPermission(ctx, "db_write_circuit_open", ...)
}
// ... 正常流程
}
WMS 波次建立参考案例¶
波次建立是理解数据安全设计的标准参考场景:AI 需要根据订单情况建立捡货波次(分配库位,创建捡货任务),这是典型的 L2 ~ L3 级别写操作.
为什么选这个场景¶
- 数据量大(十万级订单,百万级库存记录)
- 操作有业务不变量(库存不能超分配,波次内订单不能重叠)
- 操作不可即时撤销(任务一旦分配,工人可能已在路上)
- 典型的"AI 决策 + 人工确认"边界
状态机改造(对 WMS 改动量极小)¶
现有 WMS 状态机通常是:
只需增加一个"任务已建待确认"状态:
"任务已建待确认"这个状态是 AI 介入点: - 它让 WMS 不需要新增 staging 表 - 工人端看不到该状态的任务(UI 过滤) - ML 验证器可以查询所有处于该状态的任务
完整执行流程¶
1. Agent 接收波次建立请求(订单列表 + 约束条件)
2. Agent 调用 WMS API(dry_run=true)
└── WMS:BEGIN TX → 创建波次+任务 → 状态置为"任务已建待确认" → 捕获 diff → ROLLBACK
3. WMS 返回 diff(影响行数、库存分配方案、任务数量)
4. ML 验证器(事务外)
├── 检查库存分配是否合理
├── 检查波次效率指标(行走距离、时间窗口)
└── 输出通过/拒绝 + 置信度
5a. ML 通过(置信度 > 0.95)
└── Agent 调用 WMS API(dry_run=false)
└── WMS:短事务 + CAS → 真正创建任务 → 状态置为"任务已建待确认"
└── 同步审计日志
5b. ML 通过(0.7 < 置信度 < 0.95)
└── 通过 permission.Handler 发起人工确认请求
└── 操作员确认 → 同 5a
└── 操作员拒绝 → Agent 重新规划
5c. ML 拒绝(置信度 < 0.7)
└── 返回拒绝原因给 Agent,Agent 调整方案后重试(最多 3 次)
└── 3 次失败 → 熔断 + 通知操作员
6. 操作员最终确认(任意方式,如 WMS UI / API)
└── 状态变更为"可捡货",工人侧可见
四层防御映射¶
| 防御层 | WMS 场景实现 |
|---|---|
| 层 1 输出约束 | WMS API 本身的参数校验(库存不能超,时间格式等) |
| 层 2 Dry-run + ML | WMS dry_run 模式 + 波次效率 ML 模型 |
| 层 3 熔断器 | 连续 3 次 ML 拒绝 → 暂停该 Agent 的波次建立权限 |
| 层 4 Kill switch | 操作员可随时通过 WMS 管理界面暂停 AI 波次建立 |
LLM 多轮推理分支场景¶
什么时候需要分支/影子表¶
这是一个容易误判的设计点,直接说结论:
| 场景 | 是否需要分支表 | 原因 |
|---|---|---|
| 纯预测/分类(读数据 → 输出决策) | 不需要 | AI 只读数据,不写中间状态 |
| 单轮决策 + 写入 | 不需要 | Dry-run + 一次性提交即可 |
| 多轮推理:AI 需要查看自己决策应用后的中间状态 | 需要 | 见下方说明 |
多轮推理需要分支的场景¶
典型例子:AI 在建立波次时,需要多轮推理:
如果每一轮都基于真实生产数据库,轮 1 和轮 2 之间没有状态隔离: - 另一个进程的写入会干扰 AI 的推理链 - AI 无法"假设轮 1 执行后,轮 2 的数据状态是什么"
解决方案:影子表(Shadow Table)/ 会话级临时状态
本方案 2026-04-25 由 core/pkg/shadowdb/ 实现 (L437). 方案 C 列标记隔离 — 物理 shadow 表加 session_id VARCHAR(64) NOT NULL 列, 按 session_id filter 做跨 session 隔离. 原始方案 (CREATE TEMP TABLE shadow_inventory_<session_id>) 因 pooled *sql.DB 会让连接级 TEMP 表在下一条 query 上蒸发, 且 driver 语义不一致 (SQLite TEMP connection-local / MySQL TEMP 连接池不保证回收) 未采纳, 改走列标记路径获得跨 driver 一致性 + 零 DDL:
-- 1) 调用方预建物理 shadow 表 (一次性, schema 与生产对齐 + session_id 列)
CREATE TABLE shadow_inventory (
session_id VARCHAR(64) NOT NULL,
sku TEXT,
qty INTEGER
);
-- 2) shadowdb.Opener.Open 自动执行种子 SQL
INSERT INTO shadow_inventory SELECT ?, * FROM inventory; -- ? = sessionID
-- 3) AI 每轮决策带 session_id filter (由 EnforceSessionFilter 校验)
UPDATE shadow_inventory SET qty = qty - 20 WHERE sku = 'SKU-001' AND session_id = ?;
-- 4) 查询同样带 filter, 两个并发 session 看不到对方数据
SELECT * FROM shadow_inventory WHERE qty < 10 AND session_id = ?;
-- 5) 多轮推理完成, 最终方案走 staging 审批 (pkg/staging/) → 生产
-- shadowdb.Session.Close 删除 session_id=? 的所有行
DELETE FROM shadow_inventory WHERE session_id = ?;
-- 6) 崩溃错过 Close 的孤儿由 Opener.Reap(ctx, olderThan) 清理
-- (platform 层从自己的 cron 触发, core 不起 goroutine)
三层防御:
1. 工具说明层: 工具 description 要求 LLM 每个 WHERE 带 AND session_id=?
2. 参数层: shadowdb.EnforceSessionFilter(sql) quote-aware 校验 filter 存在
3. 兜底层: DB 列 session_id NOT NULL + 审计扫描串台行
何时不用分支¶
Neon 数据库分支方案(在 research-agent-database-safety.md 中被评为五星推荐)不适合生产并发写场景,理由:
- 分支之间没有实时同步:分支建立后,生产库的新写入不会自动同步到分支
- 多 Agent 并行时,每个分支的快照时间点不同,合并时有冲突
- 适合场景:开发/测试隔离,单一任务的完全独立执行(非并发生产写)
结论:Neon 分支只在 LLM 需要多轮推理查询中间状态,且任务完全隔离时有意义.生产并发写场景用影子表 + 乐观锁更合适.
已推翻的旧结论¶
记录这些是为了防止后来的人走同样的弯路.
旧结论 1:Neon 分支对生产并发写场景有效¶
旧方案:所有 AI 写操作都在 Neon 分支上执行,验证通过后合并到主库.
推翻原因: 1. 分支建立后,生产库的新写入不会实时同步到分支,导致 AI 看到的是过期快照. 2. 多个 Agent 并行时,各分支的起始时间点不同,合并时有 Write-Write 冲突,需要人工解决,成本不可控. 3. "合并"本身不是原子操作,合并期间的并发写仍然有风险.
正确场景:Neon 分支适合开发/测试环境隔离,或单一任务完全独立运行(无并发生产写)的场景.
旧结论 2:分支在所有多轮推理场景下都有必要¶
旧方案:只要 LLM 需要多轮推理,就创建数据库分支.
推翻原因:大多数多轮推理场景(如 ML 分类,参数调优)并不需要"写中间状态",只需要读.只有当 LLM 明确需要"写入决策 → 基于写入结果再次查询"这种模式时,才需要分支或影子表.
正确判断标准:LLM 是否需要查询"自己假设执行后的数据库状态".只读的多轮推理不需要任何分支机制.
引擎接口清单¶
本模块涉及的引擎接口(均已实现,消费层可直接使用):
| 接口 | 位置 | 用途 |
|---|---|---|
engine.SetSecret() |
pkg/engine/engine.go |
注入命名凭据,内存存储,不落盘 |
engine.Redact(s string) string |
pkg/engine/engine.go |
将字符串中的凭据值替换为 [credential:name](通过 Engine 公开方法访问,SecretStore 本身未导出) |
tools.DryRunnable |
pkg/tools/tool.go |
工具声明支持 Dry-run |
tools.Reversible |
pkg/tools/tool.go |
工具声明支持撤销 |
tools.CapabilityProvider |
pkg/tools/tool.go |
工具声明风险等级 |
security.AuditSink |
pkg/security/audit.go |
审计日志落地(写 DB 或 SIEM) |
security.AuditEntry |
pkg/security/audit.go |
审计记录结构(Extra map 扩展) |
permission.Handler |
pkg/permission/permission.go |
人工审批请求推送 |
engine.Config.AuditSink |
pkg/engine/config.go |
注入自定义 AuditSink |
消费层需要自行实现的部分:
- 凭据值来源(vault 读取 / 环境变量 / 配置文件),调用 SetSecret 注入
- 消费层自定义日志组件中调用 engine.Redact(s) 做脱敏(SecretStore 未导出,通过 Engine 方法访问)
- SQL 解析器(工具层 isSelectOnly 校验)
- ML 验证器(评估 Dry-run diff 的语义合理性)
- 熔断器(连续失败计数 + 权限暂停逻辑)
- 影子表管理(会话级临时表创建/清理)
- staging 表迁移任务(审批通过后的生产写入)
本文档最后更新:2026-04-05
作者:架构讨论整理(引擎设计会议)
下次更新时机:消费层 DB 写工具实现后,补充真实接口签名和测试用例路径.
SDK 编排能力设计原则¶
核心理念:正确的做法是最容易的做法
不只是提供能力,而是让安全的编排模式成为最自然的路径.消费者(如 FlySafe)不应该需要自己推导出安全编排模式--SDK 的接口,事件,文档要主动引导他们.
三个层次¶
1. CheckpointHandler 接口
针对不可逆操作,SDK 提供显式检查点机制:
engine.Run(ctx, prompt,
engine.WithCheckpointHandler(func(cp CheckpointEvent) bool {
// cp.Hint: 人类可读的风险提示
// cp.Context: 操作上下文(server、operation 等)
// cp.CanSkip: 是否允许跳过此检查点
// 返回 true = 允许继续执行;false = 拒绝(操作不会执行)
return mySystem.VerifyExternally(cp.Context)
}),
)
2. 运行时事件提示
即使消费者没有注册 CheckpointHandler,引擎识别出高风险操作时主动发出 Event:
- Type: "checkpoint_suggested"
- Hint: 人类可读的风险说明
- Context: 操作上下文
- CanSkip: 是否强制
消费者至少知道发生了什么,不是黑盒.
3. 场景化文档
文档不只写 API reference,还提供已知高风险场景的完整编排示例: - SSH 部署公钥 + 禁用密码登录(两阶段,需外部验证) - 数据库写入 + 不可逆状态变更 - 系统级配置修改
设计边界¶
| 引擎负责 | 消费层负责 |
|---|---|
| 识别高风险操作,发出 checkpoint 事件 | 注册 CheckpointHandler |
| 提供暂停/恢复执行的接口 | 实现外部验证逻辑 |
| 记录检查点结果到审计日志 | 决定是否继续 |
| 文档中提供场景示例 | 根据业务场景选择适合的编排模式 |
为什么这是竞争壁垒¶
其他 SDK 提供能力,我们提供带护栏的能力.开发者接入后自然走向安全路径,而不是需要自己踩坑后才发现安全问题.这是产品差异,不只是技术差异.