Files
selfheal-lab-apps/harness/autoapprove.go
T
seon 85d99324a1 fix(harness): atomic claim for auto-approval and rejected proposals stay rejected
보안 리뷰 반영. 자동 승인은 approved_at·executed_at·rejected_at 이 비어 있고 만료 전인
행만 조건부 UPDATE 로 선점한 경우에만 실행하므로, 사람의 승인·실행·거절이 먼저 오면
이중 실행이 없다. proposals 에 rejected_at 열을 추가해 거절 경로가 기록하고,
approve/authorize/자동 승인이 거절된 제안을 거부한다.
2026-10-15 23:41:44 +09:00

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})
}