mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-29 06:00:52 +02:00
feat: persist hitl default config
This commit is contained in:
@@ -972,6 +972,8 @@ func setupRoutes(
|
||||
protected.GET("/hitl/tool-whitelist", agentHandler.GetHITLGlobalToolWhitelist)
|
||||
protected.PUT("/hitl/tool-whitelist", agentHandler.SetHITLGlobalToolWhitelist)
|
||||
protected.POST("/hitl/tool-whitelist", agentHandler.MergeHITLGlobalToolWhitelist)
|
||||
protected.GET("/hitl/default-config", agentHandler.GetHITLDefaultConfig)
|
||||
protected.PUT("/hitl/default-config", agentHandler.UpdateHITLDefaultConfig)
|
||||
protected.GET("/hitl/default-reviewer", agentHandler.GetHITLDefaultReviewer)
|
||||
protected.PUT("/hitl/default-reviewer", agentHandler.UpdateHITLDefaultReviewer)
|
||||
protected.GET("/hitl/audit-strategy", agentHandler.GetHITLAuditStrategy)
|
||||
|
||||
@@ -1062,8 +1062,24 @@ type HitlConfig struct {
|
||||
AuditAgentPromptReviewEdit string `yaml:"audit_agent_prompt_review_edit,omitempty" json:"audit_agent_prompt_review_edit,omitempty"`
|
||||
// RetentionDays 已决策审计日志(hitl_interrupts 非 pending)保留天数;省略时默认 90;0 表示不自动清理。
|
||||
RetentionDays *int `yaml:"retention_days,omitempty" json:"retention_days,omitempty"`
|
||||
// DefaultReviewer 全局默认审批方(human | audit_agent);未选会话时切换会写入 config.yaml;新建会话无独立配置时沿用。
|
||||
// DefaultMode 全局默认人机协同模式(off | approval | review_edit);新建会话无独立配置时沿用。
|
||||
DefaultMode string `yaml:"default_mode,omitempty" json:"default_mode,omitempty"`
|
||||
// DefaultReviewer 全局默认审批方(human | audit_agent);新建会话无独立配置时沿用。
|
||||
DefaultReviewer string `yaml:"default_reviewer,omitempty" json:"default_reviewer,omitempty"`
|
||||
// DefaultTimeoutSeconds 全局默认审批等待秒数;nil 表示使用前端历史默认 300 秒,0 表示不限时。
|
||||
DefaultTimeoutSeconds *int `yaml:"default_timeout_seconds,omitempty" json:"default_timeout_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// EffectiveDefaultMode returns off, approval, or review_edit; omitted or unknown values default to off.
|
||||
func (h HitlConfig) EffectiveDefaultMode() string {
|
||||
switch strings.ToLower(strings.TrimSpace(h.DefaultMode)) {
|
||||
case "feedback", "followup":
|
||||
return "approval"
|
||||
case "approval", "review_edit":
|
||||
return strings.ToLower(strings.TrimSpace(h.DefaultMode))
|
||||
default:
|
||||
return "off"
|
||||
}
|
||||
}
|
||||
|
||||
// EffectiveDefaultReviewer returns human or audit_agent; omitted or unknown values default to human.
|
||||
@@ -1076,6 +1092,17 @@ func (h HitlConfig) EffectiveDefaultReviewer() string {
|
||||
}
|
||||
}
|
||||
|
||||
// EffectiveDefaultTimeoutSeconds returns the default HITL approval timeout; nil defaults to 5 minutes.
|
||||
func (h HitlConfig) EffectiveDefaultTimeoutSeconds() int {
|
||||
if h.DefaultTimeoutSeconds == nil {
|
||||
return 300
|
||||
}
|
||||
if *h.DefaultTimeoutSeconds < 0 {
|
||||
return 0
|
||||
}
|
||||
return *h.DefaultTimeoutSeconds
|
||||
}
|
||||
|
||||
// RetentionDaysEffective returns retention; 0 means keep forever; omitted defaults to 90.
|
||||
func (h HitlConfig) RetentionDaysEffective() int {
|
||||
if h.RetentionDays == nil {
|
||||
|
||||
@@ -95,6 +95,29 @@ func TestHitlAuditModelEffectiveFallsBackToMainConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHitlDefaultConfigEffectiveValues(t *testing.T) {
|
||||
if got := (HitlConfig{}).EffectiveDefaultMode(); got != "off" {
|
||||
t.Fatalf("empty default mode = %q, want off", got)
|
||||
}
|
||||
if got := (HitlConfig{DefaultMode: "review-edit"}).EffectiveDefaultMode(); got != "off" {
|
||||
t.Fatalf("unknown default mode = %q, want off", got)
|
||||
}
|
||||
if got := (HitlConfig{DefaultMode: "review_edit"}).EffectiveDefaultMode(); got != "review_edit" {
|
||||
t.Fatalf("review_edit default mode = %q, want review_edit", got)
|
||||
}
|
||||
if got := (HitlConfig{}).EffectiveDefaultTimeoutSeconds(); got != 300 {
|
||||
t.Fatalf("empty default timeout = %d, want 300", got)
|
||||
}
|
||||
zero := 0
|
||||
if got := (HitlConfig{DefaultTimeoutSeconds: &zero}).EffectiveDefaultTimeoutSeconds(); got != 0 {
|
||||
t.Fatalf("zero default timeout = %d, want 0", got)
|
||||
}
|
||||
neg := -1
|
||||
if got := (HitlConfig{DefaultTimeoutSeconds: &neg}).EffectiveDefaultTimeoutSeconds(); got != 0 {
|
||||
t.Fatalf("negative default timeout = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesAIDefaultChannelAsRuntimeOpenAI(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
@@ -315,12 +315,13 @@ func (h *AgentHandler) SetHitlToolWhitelistSaver(s HitlToolWhitelistSaver) {
|
||||
h.hitlWhitelistSaver = s
|
||||
}
|
||||
|
||||
// HitlDefaultReviewerSaver 持久化全局默认审批方到 config.yaml。
|
||||
// HitlDefaultReviewerSaver 持久化全局默认人机协同配置到 config.yaml。
|
||||
type HitlDefaultReviewerSaver interface {
|
||||
UpdateHitlDefaultConfig(mode, reviewer string, timeoutSeconds int) error
|
||||
UpdateHitlDefaultReviewer(reviewer string) error
|
||||
}
|
||||
|
||||
// SetHitlDefaultReviewerSaver 设置 HITL 默认审批方落盘。
|
||||
// SetHitlDefaultReviewerSaver 设置 HITL 默认配置落盘。
|
||||
func (h *AgentHandler) SetHitlDefaultReviewerSaver(s HitlDefaultReviewerSaver) {
|
||||
h.hitlDefaultReviewerSaver = s
|
||||
}
|
||||
@@ -332,6 +333,35 @@ func (h *AgentHandler) hitlEffectiveDefaultReviewer() string {
|
||||
return "human"
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlEffectiveDefaultMode() string {
|
||||
if h != nil && h.config != nil {
|
||||
return normalizeHitlDefaultMode(h.config.Hitl.EffectiveDefaultMode())
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlEffectiveDefaultTimeoutSeconds() int {
|
||||
if h != nil && h.config != nil {
|
||||
timeout := h.config.Hitl.EffectiveDefaultTimeoutSeconds()
|
||||
if timeout < 0 {
|
||||
return 0
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
return 300
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlEffectiveDefaultRequest() *HITLRequest {
|
||||
mode := h.hitlEffectiveDefaultMode()
|
||||
return &HITLRequest{
|
||||
Enabled: mode != "off",
|
||||
Mode: mode,
|
||||
Reviewer: h.hitlEffectiveDefaultReviewer(),
|
||||
SensitiveTools: []string{},
|
||||
TimeoutSeconds: h.hitlEffectiveDefaultTimeoutSeconds(),
|
||||
}
|
||||
}
|
||||
|
||||
// HITLNeedsToolApproval 供 C2 危险任务门控:与会话侧人机协同及免审批白名单判定一致。
|
||||
func (h *AgentHandler) HITLNeedsToolApproval(conversationID, toolName string) bool {
|
||||
if h == nil || h.hitlManager == nil {
|
||||
|
||||
@@ -891,7 +891,14 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
if req.Hitl != nil {
|
||||
h.config.Hitl.AuditModel = req.Hitl.AuditModel
|
||||
h.config.Hitl.ToolWhitelist = mergeHitlToolWhitelistSlice(nil, req.Hitl.ToolWhitelist)
|
||||
if strings.TrimSpace(req.Hitl.DefaultMode) != "" {
|
||||
h.config.Hitl.DefaultMode = req.Hitl.EffectiveDefaultMode()
|
||||
}
|
||||
h.config.Hitl.DefaultReviewer = req.Hitl.EffectiveDefaultReviewer()
|
||||
if req.Hitl.DefaultTimeoutSeconds != nil {
|
||||
v := req.Hitl.EffectiveDefaultTimeoutSeconds()
|
||||
h.config.Hitl.DefaultTimeoutSeconds = &v
|
||||
}
|
||||
h.config.Hitl.AuditAgentPrompt = strings.TrimSpace(req.Hitl.AuditAgentPrompt)
|
||||
h.config.Hitl.AuditAgentPromptReviewEdit = strings.TrimSpace(req.Hitl.AuditAgentPromptReviewEdit)
|
||||
if req.Hitl.RetentionDays != nil {
|
||||
@@ -2141,12 +2148,35 @@ func updateHitlConfig(doc *yaml.Node, cfg config.HitlConfig) {
|
||||
setStringInMap(auditModelNode, "model", cfg.AuditModel.Model)
|
||||
// flow 样式 [a, b, c] 单行展示,工具多时比块序列省行数
|
||||
setFlowStringSliceInMap(hitlNode, "tool_whitelist", cfg.ToolWhitelist)
|
||||
setStringInMap(hitlNode, "default_mode", cfg.EffectiveDefaultMode())
|
||||
setStringInMap(hitlNode, "default_reviewer", cfg.EffectiveDefaultReviewer())
|
||||
setIntInMap(hitlNode, "default_timeout_seconds", cfg.EffectiveDefaultTimeoutSeconds())
|
||||
setIntInMap(hitlNode, "retention_days", cfg.RetentionDaysEffective())
|
||||
setStringInMap(hitlNode, "audit_agent_prompt", cfg.AuditAgentPrompt)
|
||||
setStringInMap(hitlNode, "audit_agent_prompt_review_edit", cfg.AuditAgentPromptReviewEdit)
|
||||
}
|
||||
|
||||
// UpdateHitlDefaultConfig 更新全局默认人机协同配置并写入 config.yaml。
|
||||
func (h *ConfigHandler) UpdateHitlDefaultConfig(mode, reviewer string, timeoutSeconds int) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.config.Hitl.DefaultMode = config.HitlConfig{DefaultMode: mode}.EffectiveDefaultMode()
|
||||
h.config.Hitl.DefaultReviewer = config.HitlConfig{DefaultReviewer: reviewer}.EffectiveDefaultReviewer()
|
||||
if timeoutSeconds < 0 {
|
||||
timeoutSeconds = 0
|
||||
}
|
||||
h.config.Hitl.DefaultTimeoutSeconds = &timeoutSeconds
|
||||
if err := h.saveConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.logger.Info("HITL 全局默认配置已写入配置文件",
|
||||
zap.String("default_mode", h.config.Hitl.DefaultMode),
|
||||
zap.String("default_reviewer", h.config.Hitl.DefaultReviewer),
|
||||
zap.Int("default_timeout_seconds", timeoutSeconds),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateHitlDefaultReviewer 更新全局默认审批方并写入 config.yaml。
|
||||
func (h *ConfigHandler) UpdateHitlDefaultReviewer(reviewer string) error {
|
||||
h.mu.Lock()
|
||||
|
||||
@@ -289,6 +289,18 @@ func normalizeHitlMode(mode string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHitlDefaultMode(mode string) string {
|
||||
v := strings.ToLower(strings.TrimSpace(mode))
|
||||
switch v {
|
||||
case "feedback", "followup":
|
||||
return "approval"
|
||||
case "approval", "review_edit":
|
||||
return v
|
||||
default:
|
||||
return "off"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *HITLManager) ActivateConversation(conversationID string, req *HITLRequest) {
|
||||
if req == nil || !req.Enabled {
|
||||
m.DeactivateConversation(conversationID)
|
||||
@@ -629,7 +641,7 @@ func (h *AgentHandler) loadHITLConversationConfig(conversationID string) (*HITLR
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
cfg.Reviewer = h.hitlEffectiveDefaultReviewer()
|
||||
return h.hitlEffectiveDefaultRequest(), nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -994,7 +1006,9 @@ func (h *AgentHandler) GetHITLConversationConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"conversationId": conversationID,
|
||||
"hitl": cfg,
|
||||
"defaultMode": h.hitlEffectiveDefaultMode(),
|
||||
"defaultReviewer": h.hitlEffectiveDefaultReviewer(),
|
||||
"defaultTimeoutSeconds": h.hitlEffectiveDefaultTimeoutSeconds(),
|
||||
"hitlGlobalToolWhitelist": h.hitlConfigGlobalToolWhitelist(),
|
||||
})
|
||||
}
|
||||
@@ -1051,11 +1065,64 @@ type setHitlDefaultReviewerReq struct {
|
||||
Reviewer string `json:"reviewer"`
|
||||
}
|
||||
|
||||
type setHitlDefaultConfigReq struct {
|
||||
Mode string `json:"mode"`
|
||||
Reviewer string `json:"reviewer"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds"`
|
||||
}
|
||||
|
||||
func (h *AgentHandler) hitlDefaultConfigResponse() gin.H {
|
||||
return gin.H{
|
||||
"defaultMode": h.hitlEffectiveDefaultMode(),
|
||||
"defaultReviewer": h.hitlEffectiveDefaultReviewer(),
|
||||
"defaultTimeoutSeconds": h.hitlEffectiveDefaultTimeoutSeconds(),
|
||||
"hitlGlobalToolWhitelist": h.hitlConfigGlobalToolWhitelist(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetHITLDefaultConfig 返回 config.yaml 中的全局默认人机协同配置。
|
||||
func (h *AgentHandler) GetHITLDefaultConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, h.hitlDefaultConfigResponse())
|
||||
}
|
||||
|
||||
// UpdateHITLDefaultConfig 将全局默认人机协同配置写入 config.yaml。
|
||||
func (h *AgentHandler) UpdateHITLDefaultConfig(c *gin.Context) {
|
||||
if h.hitlDefaultReviewerSaver == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "HITL 配置持久化不可用"})
|
||||
return
|
||||
}
|
||||
var req setHitlDefaultConfigReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mode := normalizeHitlDefaultMode(req.Mode)
|
||||
reviewer := normalizeHitlReviewer(req.Reviewer)
|
||||
timeoutSeconds := req.TimeoutSeconds
|
||||
if timeoutSeconds < 0 {
|
||||
timeoutSeconds = 0
|
||||
}
|
||||
if err := h.hitlDefaultReviewerSaver.UpdateHitlDefaultConfig(mode, reviewer, timeoutSeconds); err != nil {
|
||||
h.logger.Warn("写入 HITL 默认配置到 config.yaml 失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if h.config != nil {
|
||||
h.config.Hitl.DefaultMode = mode
|
||||
h.config.Hitl.DefaultReviewer = reviewer
|
||||
h.config.Hitl.DefaultTimeoutSeconds = &timeoutSeconds
|
||||
}
|
||||
if h.audit != nil {
|
||||
h.audit.RecordOK(c, "hitl", "default_config_update", "HITL 全局默认配置更新", "hitl_config", "default", nil)
|
||||
}
|
||||
out := h.hitlDefaultConfigResponse()
|
||||
out["ok"] = true
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// GetHITLDefaultReviewer 返回 config.yaml 中的全局默认审批方。
|
||||
func (h *AgentHandler) GetHITLDefaultReviewer(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"defaultReviewer": h.hitlEffectiveDefaultReviewer(),
|
||||
})
|
||||
c.JSON(http.StatusOK, h.hitlDefaultConfigResponse())
|
||||
}
|
||||
|
||||
// UpdateHITLDefaultReviewer 将全局默认审批方写入 config.yaml(未选会话时切换审批方)。
|
||||
@@ -1081,10 +1148,9 @@ func (h *AgentHandler) UpdateHITLDefaultReviewer(c *gin.Context) {
|
||||
if h.audit != nil {
|
||||
h.audit.RecordOK(c, "hitl", "default_reviewer_update", "HITL 全局默认审批方更新", "hitl_config", "default_reviewer", nil)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true,
|
||||
"defaultReviewer": reviewer,
|
||||
})
|
||||
out := h.hitlDefaultConfigResponse()
|
||||
out["ok"] = true
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// SetHITLGlobalToolWhitelist 整表替换 config.yaml 中的全局免审批工具白名单。
|
||||
|
||||
@@ -217,7 +217,7 @@ func resourceAllowed(c *gin.Context, db *database.DB) bool {
|
||||
return session.Scope == database.RBACScopeAll
|
||||
case strings.HasPrefix(path, "/c2/profiles") && c.Request.Method != http.MethodGet:
|
||||
return session.Scope == database.RBACScopeAll
|
||||
case (strings.HasPrefix(path, "/hitl/tool-whitelist") || strings.HasPrefix(path, "/hitl/default-reviewer") || strings.HasPrefix(path, "/hitl/audit-strategy")) && c.Request.Method != http.MethodGet:
|
||||
case (strings.HasPrefix(path, "/hitl/tool-whitelist") || strings.HasPrefix(path, "/hitl/default-config") || strings.HasPrefix(path, "/hitl/default-reviewer") || strings.HasPrefix(path, "/hitl/audit-strategy")) && c.Request.Method != http.MethodGet:
|
||||
return session.Scope == database.RBACScopeAll
|
||||
case isMutationMethod(c.Request.Method) && isProcessGlobalMutationPath(path):
|
||||
// These definitions/configurations are shared by every user and do not
|
||||
|
||||
Reference in New Issue
Block a user