feat(common): add guarded operation executor

This commit is contained in:
NeeNe 2026-07-10 16:43:47 +08:00
parent c1f156f7df
commit c9ae4c70b8
2 changed files with 301 additions and 0 deletions

214
shortcuts/common/guarded.go Normal file
View File

@ -0,0 +1,214 @@
package common
import (
"errors"
"fmt"
"strings"
)
type RiskLevel string
const (
RiskLow RiskLevel = "low"
RiskMedium RiskLevel = "medium"
RiskHigh RiskLevel = "high"
)
const redactedValue = "<redacted>"
var ErrConfirmationRequired = errors.New("explicit confirmation is required")
type ExitCoder interface {
ExitCode() int
}
type ConfirmationError struct {
Operation string
Risk RiskLevel
}
func (e *ConfirmationError) Error() string {
return fmt.Sprintf("%s: %v for %s-risk operation", e.Operation, ErrConfirmationRequired, e.Risk)
}
func (e *ConfirmationError) Unwrap() error { return ErrConfirmationRequired }
func (e *ConfirmationError) ExitCode() int { return 2 }
type GuardedOperation struct {
Name string
Risk RiskLevel
DryRun bool
Confirmed bool
RequireConfirmation bool
Preview interface{}
SensitiveFields []string
}
type GuardedOperationResult struct {
Operation string `json:"operation"`
Risk RiskLevel `json:"risk"`
DryRun bool `json:"dry_run"`
Executed bool `json:"executed"`
Preview interface{} `json:"preview,omitempty"`
Data interface{} `json:"data,omitempty"`
}
func (op GuardedOperation) Execute(run func() (interface{}, error)) (*GuardedOperationResult, error) {
op = normalizeGuardedOperation(op)
result := &GuardedOperationResult{
Operation: op.Name,
Risk: op.Risk,
DryRun: op.DryRun,
Preview: RedactSensitive(op.Preview, op.SensitiveFields),
}
if op.DryRun {
return result, nil
}
if (op.RequireConfirmation || op.Risk == RiskHigh) && !op.Confirmed {
return result, &ConfirmationError{Operation: op.Name, Risk: op.Risk}
}
data, err := run()
if err != nil {
return result, err
}
result.Executed = true
result.Data = RedactSensitive(data, op.SensitiveFields)
return result, nil
}
type BatchOperation struct {
ID string
Preview interface{}
Run func() (interface{}, error)
}
type BatchOptions struct {
Guard GuardedOperation
ContinueOnError bool
}
type BatchItemResult struct {
ID string `json:"id"`
Status string `json:"status"`
Preview interface{} `json:"preview,omitempty"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type BatchResult struct {
Operation string `json:"operation"`
Risk RiskLevel `json:"risk"`
DryRun bool `json:"dry_run"`
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Items []BatchItemResult `json:"items"`
}
type BatchError struct {
Failed int
Total int
}
func (e *BatchError) Error() string {
return fmt.Sprintf("batch operation failed for %d of %d items", e.Failed, e.Total)
}
func (e *BatchError) ExitCode() int { return 3 }
func ExecuteBatch(operations []BatchOperation, options BatchOptions) (*BatchResult, error) {
guard := normalizeGuardedOperation(options.Guard)
result := &BatchResult{
Operation: guard.Name,
Risk: guard.Risk,
DryRun: guard.DryRun,
Total: len(operations),
Items: make([]BatchItemResult, 0, len(operations)),
}
if !guard.DryRun && (guard.RequireConfirmation || guard.Risk == RiskHigh) && !guard.Confirmed {
result.Skipped = len(operations)
return result, &ConfirmationError{Operation: guard.Name, Risk: guard.Risk}
}
for index, operation := range operations {
item := BatchItemResult{
ID: operation.ID,
Preview: RedactSensitive(operation.Preview, guard.SensitiveFields),
}
if guard.DryRun {
item.Status = "skipped"
result.Skipped++
result.Items = append(result.Items, item)
continue
}
data, err := operation.Run()
if err != nil {
item.Status = "failed"
item.Error = err.Error()
result.Failed++
result.Items = append(result.Items, item)
if !options.ContinueOnError {
remaining := len(operations) - index - 1
result.Skipped += remaining
for _, skipped := range operations[index+1:] {
result.Items = append(result.Items, BatchItemResult{ID: skipped.ID, Status: "skipped", Preview: RedactSensitive(skipped.Preview, guard.SensitiveFields)})
}
break
}
continue
}
item.Status = "succeeded"
item.Data = RedactSensitive(data, guard.SensitiveFields)
result.Succeeded++
result.Items = append(result.Items, item)
}
if result.Failed > 0 {
return result, &BatchError{Failed: result.Failed, Total: result.Total}
}
return result, nil
}
func normalizeGuardedOperation(op GuardedOperation) GuardedOperation {
if op.Name == "" {
op.Name = "operation"
}
if op.Risk == "" {
op.Risk = RiskMedium
}
return op
}
func RedactSensitive(value interface{}, extraFields []string) interface{} {
sensitive := map[string]struct{}{
"token": {}, "access_token": {}, "authorization": {}, "password": {},
"secret": {}, "api_key": {}, "private_key": {}, "cookie": {},
}
for _, field := range extraFields {
sensitive[strings.ToLower(strings.TrimSpace(field))] = struct{}{}
}
return redactValue(value, sensitive)
}
func redactValue(value interface{}, sensitive map[string]struct{}) interface{} {
switch typed := value.(type) {
case map[string]interface{}:
redacted := make(map[string]interface{}, len(typed))
for key, child := range typed {
if _, ok := sensitive[strings.ToLower(key)]; ok {
redacted[key] = redactedValue
} else {
redacted[key] = redactValue(child, sensitive)
}
}
return redacted
case []interface{}:
redacted := make([]interface{}, len(typed))
for index, child := range typed {
redacted[index] = redactValue(child, sensitive)
}
return redacted
default:
return value
}
}

View File

@ -0,0 +1,87 @@
package common
import (
"errors"
"testing"
)
func TestGuardedOperationDryRunDoesNotExecuteAndRedacts(t *testing.T) {
executed := false
result, err := (GuardedOperation{
Name: "delete", Risk: RiskHigh, DryRun: true,
Preview: map[string]interface{}{"id": 1, "token": "secret"},
}).Execute(func() (interface{}, error) {
executed = true
return nil, nil
})
if err != nil || executed || result.Executed {
t.Fatalf("result=%+v executed=%v err=%v", result, executed, err)
}
if result.Preview.(map[string]interface{})["token"] != redactedValue {
t.Fatal("sensitive preview was not redacted")
}
}
func TestGuardedOperationRequiresExplicitConfirmation(t *testing.T) {
result, err := (GuardedOperation{Name: "delete", Risk: RiskHigh}).Execute(func() (interface{}, error) {
return nil, nil
})
if err == nil || !errors.Is(err, ErrConfirmationRequired) || result.Executed {
t.Fatalf("result=%+v err=%v", result, err)
}
var exitCoder ExitCoder
if !errors.As(err, &exitCoder) || exitCoder.ExitCode() != 2 {
t.Fatalf("confirmation exit code = %v", err)
}
}
func TestGuardedOperationExecutesConfirmedWrite(t *testing.T) {
result, err := (GuardedOperation{Name: "delete", Risk: RiskHigh, Confirmed: true}).Execute(func() (interface{}, error) {
return map[string]interface{}{"deleted": true}, nil
})
if err != nil || !result.Executed || result.Data.(map[string]interface{})["deleted"] != true {
t.Fatalf("result=%+v err=%v", result, err)
}
}
func TestExecuteBatchContinueOnErrorSummarizesPartialSuccess(t *testing.T) {
operations := []BatchOperation{
{ID: "1", Run: func() (interface{}, error) { return map[string]interface{}{"password": "secret"}, nil }},
{ID: "2", Run: func() (interface{}, error) { return nil, errors.New("failed") }},
{ID: "3", Run: func() (interface{}, error) { return "ok", nil }},
}
result, err := ExecuteBatch(operations, BatchOptions{
Guard: GuardedOperation{Name: "batch", Risk: RiskMedium}, ContinueOnError: true,
})
if err == nil || result.Succeeded != 2 || result.Failed != 1 || result.Skipped != 0 {
t.Fatalf("result=%+v err=%v", result, err)
}
if result.Items[0].Data.(map[string]interface{})["password"] != redactedValue {
t.Fatal("batch result was not redacted")
}
var exitCoder ExitCoder
if !errors.As(err, &exitCoder) || exitCoder.ExitCode() != 3 {
t.Fatalf("batch exit code = %v", err)
}
}
func TestExecuteBatchStopsAndMarksRemainingItemsSkipped(t *testing.T) {
executedThird := false
operations := []BatchOperation{
{ID: "1", Run: func() (interface{}, error) { return nil, errors.New("failed") }},
{ID: "2", Run: func() (interface{}, error) { executedThird = true; return nil, nil }},
}
result, err := ExecuteBatch(operations, BatchOptions{Guard: GuardedOperation{Name: "batch"}})
if err == nil || result.Failed != 1 || result.Skipped != 1 || executedThird {
t.Fatalf("result=%+v executed=%v err=%v", result, executedThird, err)
}
}
func TestExecuteBatchDryRunSkipsAllItems(t *testing.T) {
result, err := ExecuteBatch([]BatchOperation{{ID: "1"}, {ID: "2"}}, BatchOptions{
Guard: GuardedOperation{Name: "batch", DryRun: true},
})
if err != nil || result.Skipped != 2 || result.Succeeded != 0 || result.Failed != 0 {
t.Fatalf("result=%+v err=%v", result, err)
}
}