fix(harness): atomic claim for auto-approval and rejected proposals stay rejected

보안 리뷰 반영. 자동 승인은 approved_at·executed_at·rejected_at 이 비어 있고 만료 전인
행만 조건부 UPDATE 로 선점한 경우에만 실행하므로, 사람의 승인·실행·거절이 먼저 오면
이중 실행이 없다. proposals 에 rejected_at 열을 추가해 거절 경로가 기록하고,
approve/authorize/자동 승인이 거절된 제안을 거부한다.
This commit is contained in:
seon committed 2026-10-15 23:41:44 +09:00
1 parent ca933a646e
commit 85d99324a1
5 files changed
+34 -8

No files matched your search

+7 -5
View File
@@ -9,16 +9,20 @@ import (
// 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.
// 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 || p.ApprovedAt != nil || time.Now().After(p.ExpiresAt) {
if err != nil {
return
}
if err := a.store.ApproveProposal(ctx, token, "auto"); err != nil {
// 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:]
@@ -31,7 +35,6 @@ func (a *App) autoApprove(token string, req ProposalRequest, after time.Duration
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()
_ = a.store.MarkProposalExecuted(ctx, token)
return
}
res, err := a.executeAction(ctx, p.ActionID, req.Target, "ai-approved", p.RunID, p.ScenarioID)
@@ -41,5 +44,4 @@ func (a *App) autoApprove(token string, req ProposalRequest, after time.Duration
}
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})
_ = a.store.MarkProposalExecuted(ctx, token)
}