보안 리뷰 반영. 자동 승인은 approved_at·executed_at·rejected_at 이 비어 있고 만료 전인 행만 조건부 UPDATE 로 선점한 경우에만 실행하므로, 사람의 승인·실행·거절이 먼저 오면 이중 실행이 없다. proposals 에 rejected_at 열을 추가해 거절 경로가 기록하고, approve/authorize/자동 승인이 거절된 제안을 거부한다.
48 lines
2.2 KiB
Go
48 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// autoApprove is the measurement-mode stand-in for the Discord approver (run option auto_approve_after_s, 2026-10-15):
|
|
// after the configured delay the harness approves the layer-2 proposal (actor "auto") and executes it exactly as the
|
|
// HITL bot does after a button press (actor "ai-approved", MarkProposalExecuted). MTTA therefore equals the delay;
|
|
// the verdict logic is unchanged (layer 2 from the ai-approved action). A proposal already decided, expired or
|
|
// recorded-only is handled the same way as the API path. The claim (approve + executed_at) is a single conditional
|
|
// UPDATE, so a human approval/execution or a rejection that arrives first takes precedence (no double execution).
|
|
func (a *App) autoApprove(token string, req ProposalRequest, after time.Duration) {
|
|
time.Sleep(after)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
p, err := a.store.GetProposal(ctx, token)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// one conditional update claims approval + execution: a human/bot decision (approve, reject, execute) that lands
|
|
// first wins and this goroutine does nothing
|
|
claimed, err := a.store.ClaimProposal(ctx, token, "auto")
|
|
if err != nil || !claimed {
|
|
return
|
|
}
|
|
suffix := token[len(token)-6:]
|
|
a.store.Event(ctx, p.RunID, p.ScenarioID, "approval", "auto", map[string]any{"action_id": p.ActionID, "token_suffix": suffix, "auto_approve_after_s": after.Seconds()})
|
|
ce := catalogEntry(p.ActionID)
|
|
if ce == nil {
|
|
return
|
|
}
|
|
note := "auto-approved by the harness (measurement mode)"
|
|
if !ce.Exec {
|
|
a.store.Event(ctx, p.RunID, p.ScenarioID, "action", "ai-approved", map[string]any{"action_id": p.ActionID, "target": req.Target, "recorded_only": true, "note": note, "layer": ce.Layer})
|
|
mActs.WithLabelValues(p.ActionID, "ai-approved", "recorded").Inc()
|
|
return
|
|
}
|
|
res, err := a.executeAction(ctx, p.ActionID, req.Target, "ai-approved", p.RunID, p.ScenarioID)
|
|
result := "ok"
|
|
if err != nil {
|
|
result = "error"
|
|
}
|
|
mActs.WithLabelValues(p.ActionID, "ai-approved", result).Inc()
|
|
a.store.Event(ctx, p.RunID, p.ScenarioID, "action", "ai-approved", map[string]any{"action_id": p.ActionID, "target": req.Target, "result": res, "error": errStr(err), "note": note})
|
|
}
|