Files
so-many-crows/approve.go
T
SmokeyandClaude Opus 5.5 940c74672a Add web server, event stream, and Rive contract
- crows serve: HTTP API, Server-Sent Events per list, web approvals
  with a 10-minute fail-safe timeout; localhost only by default
- Shared pipeline for command line and server; trust.changed events
- EVENTS.md: server/UI contract; RIVE_CONTRACT.md: data-binding spec
  for the animator (one Crow artboard for v1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
2026-10-27 08:31:27 -06:00

98 lines
2.1 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"strings"
"sync"
"time"
)
// StdinApprover asks at the terminal. It fails safe: anything but y/yes,
// including no input at all, is a no.
type StdinApprover struct {
mu sync.Mutex
in *bufio.Reader
}
func (s *StdinApprover) Approve(ctx context.Context, a Approval) string {
s.mu.Lock()
defer s.mu.Unlock()
prompt := " Accept this answer?"
if a.Target == "triage" {
prompt = " Approve triage?"
}
if confirm(s.in, prompt) {
return "approved"
}
return "rejected"
}
// confirm fails safe: anything but y/yes, including no input at all, is a no.
func confirm(in *bufio.Reader, prompt string) bool {
fmt.Print(prompt + " [y/N] ")
line, _ := in.ReadString('\n')
a := strings.ToLower(strings.TrimSpace(line))
if line == "" {
fmt.Println()
}
return a == "y" || a == "yes"
}
// WebApprover blocks an item's goroutine until POST /api/verdicts answers it,
// or the timeout passes, which counts as a rejection.
type WebApprover struct {
Timeout time.Duration
mu sync.Mutex
pending map[string]chan bool // key: item_id|target
}
func NewWebApprover(timeout time.Duration) *WebApprover {
return &WebApprover{Timeout: timeout, pending: map[string]chan bool{}}
}
func approvalKey(itemID, target string) string { return itemID + "|" + target }
func (w *WebApprover) Approve(ctx context.Context, a Approval) string {
key := approvalKey(a.ItemID, a.Target)
ch := make(chan bool, 1)
w.mu.Lock()
w.pending[key] = ch
w.mu.Unlock()
defer func() {
w.mu.Lock()
delete(w.pending, key)
w.mu.Unlock()
}()
timer := time.NewTimer(w.Timeout)
defer timer.Stop()
select {
case ok := <-ch:
if ok {
return "approved"
}
return "rejected"
case <-timer.C:
return "timeout"
case <-ctx.Done():
return "rejected"
}
}
// Resolve delivers a human's verdict. It reports false if nothing is waiting.
func (w *WebApprover) Resolve(itemID, target string, approve bool) bool {
key := approvalKey(itemID, target)
w.mu.Lock()
defer w.mu.Unlock()
ch, ok := w.pending[key]
if !ok {
return false
}
delete(w.pending, key) // one verdict per question
ch <- approve
return true
}