Files
heicode-mananger/heicode/model/preflight_confirmation.go
T
chenchenandClaude Opus 4.8 19640b44f5 fix(preflight): address #41 review — persisted confirmation, deploy ready re-check, template-aware version
回应 Fasthei 复审(PR #51 CHANGES_REQUESTED):
1. 持久化确认记录(强一致):新增 model.PreflightConfirmation 表 + InsertPreflightConfirmation +
   PreflightConfirmationExists。confirm 时落库(默认 TTL=HEICODE_PREFLIGHT_CONFIRMATION_TTL_SECONDS
   =3600s,可设 0 不过期),写失败直接报错(非 best-effort)。部署侧要求该版本存在未过期确认记录
   → 杜绝直接拿 GET version 绕过 confirm/审计。
2. 部署重新校验 Ready:verifyDeployPreflight 增加 summary.Ready 检查 —— 预算/agent_slot 等
   易变项不进版本哈希,故部署时重查,防 confirm 后余额耗尽/槽位占满仍启动。
3. 版本哈希纳入模板安全面:computePreflightVersion 加 tplDigest(definition+model+name 摘要),
   管理员改同一 template_key 的 definition/model 后旧确认失效。补 TestComputePreflightVersion_ChangesOnTemplateEdit。
4. 审计降为附加流:强一致确认记录作为部署门禁;审计 preflight.confirmed 互补。

测试:PreflightConfirmationExists(命中/版本不符/跨用户/过期/不过期/空参)、PreflightBindingKey、
模板变更翻转版本。TestMain + 生产迁移注册 PreflightConfirmation。controller+model 全回归通过。
文档 §4.1.1 更新。

Affects: Manager only(新增 preflight_confirmations 表 + 部署门禁强化)。无计费改动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:28:07 +08:00

71 lines
2.8 KiB
Go

package model
import (
"sort"
"strconv"
"strings"
)
// PreflightConfirmation 持久化一次 preflight「已确认版本」记录(#41 复审 #1/#4)。
//
// 为什么需要它:仅靠「部署时重算 hash == 传入 version」无法证明这个 version 曾被 confirm ——
// GET /api/heicode/preflight 也会返回同一个 version,客户端可绕过 confirm 与审计直接拿 GET
// version 去部署。把 confirm 落成一条**强一致、可查询**的记录,部署时校验该记录确实存在且未过期,
// 才真正满足「启动接口校验已确认版本」。审计事件(agent_audit_events)作为附加审计流。
type PreflightConfirmation struct {
Id int `gorm:"primaryKey" json:"id"`
UserID int `gorm:"index" json:"user_id"`
TemplateID string `gorm:"type:varchar(64);index" json:"template_id"`
BindingKey string `gorm:"type:varchar(512);index" json:"binding_key"` // 归一化排序后的 binding ids,便于审计/排查
Version string `gorm:"type:varchar(64);index" json:"version"` // 防篡改摘要版本
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
ExpiresAtMs int64 `gorm:"bigint;index" json:"expires_at_ms"` // 0 表示不过期
}
func (PreflightConfirmation) TableName() string { return "preflight_confirmations" }
// PreflightBindingKey 把绑定 id 归一化(去重/去非正/升序)后拼成稳定 key,confirm 与 deploy
// 用同一算法,保证同一组绑定得到同一 key。
func PreflightBindingKey(bindingIDs []int) string {
seen := map[int]bool{}
ids := make([]int, 0, len(bindingIDs))
for _, n := range bindingIDs {
if n > 0 && !seen[n] {
seen[n] = true
ids = append(ids, n)
}
}
sort.Ints(ids)
parts := make([]string, 0, len(ids))
for _, n := range ids {
parts = append(parts, strconv.Itoa(n))
}
return strings.Join(parts, ",")
}
// InsertPreflightConfirmation 持久化一条确认记录。
func InsertPreflightConfirmation(rec *PreflightConfirmation) error {
if DB == nil || rec == nil {
return nil
}
return DB.Create(rec).Error
}
// PreflightConfirmationExists 校验存在一条匹配的、未过期的确认记录(#41 部署侧强校验)。
// 匹配 user + template + version(version 已编码资源+模板安全面);nowMs 用于过期判断,便于单测。
func PreflightConfirmationExists(userID int, templateID, version string, nowMs int64) (bool, error) {
if DB == nil {
return false, nil
}
version = strings.TrimSpace(version)
if userID <= 0 || strings.TrimSpace(templateID) == "" || version == "" {
return false, nil
}
var count int64
err := DB.Model(&PreflightConfirmation{}).
Where("user_id = ? AND template_id = ? AND version = ?", userID, templateID, version).
Where("expires_at_ms = 0 OR expires_at_ms > ?", nowMs).
Count(&count).Error
return count > 0, err
}