回应 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>
71 lines
2.8 KiB
Go
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
|
|
}
|