package audit import ( "context" "errors" "fmt" "log/slog" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // Sentinel errors returned by Approver. Use errors.Is to test for them. var ( // ErrActionMissing is returned when the action is blank. ErrActionMissing = errors.New("audit: action is missing") // ErrUnknownActor is returned when the acting role name is blank. This // package has no role registry, so a blank name is the only "unknown" it // can detect; whether a role is truly authorized is decided upstream. ErrUnknownActor = errors.New("audit: actor is unknown") // ErrExplainMissing is returned when the explanation is blank. ErrExplainMissing = errors.New("audit: explanation is missing") ) // reviewerMain is the name recorded for the main agent that performs the review. const reviewerMain = "main" // Approval is the immutable record of a single main-agent review-and-approve of // a subagent's sensitive action. type Approval struct { // Actor is the acting role name (角色名). Actor string // Action is the sensitive action being approved. Action string // Description is the explanation of what the actor wants to do (what the // action means). It is named Description rather than Explain because the // Explain method (below) returns the frontend notice. Description string // Allowed reports whether the action was granted. Allowed bool // Reviewer is the main agent that performed the review. Reviewer string // At is when the review-and-approve happened. At time.Time } // Explain returns the short frontend notice describing the review outcome. For // an allowed action it is "「 想要干什么,我审查后批准了」"; for a denied // action it uses "我审查后拒绝了". Actor is interpolated in place of . func (a Approval) Explain() string { if a.Allowed { return "「" + a.Actor + " 想要干什么,我审查后批准了」" } return "「" + a.Actor + " 想要干什么,我审查后拒绝了」" } // Approver records main-agent review-and-approve events and their audit logs. type Approver struct { log *slog.Logger } // NewApprover builds an Approver that writes audit logs through log. The logger // is tagged with source "orchestration/audit". A nil logger is allowed, making // the approver record silently (no log output). func NewApprover(log *slog.Logger) *Approver { return &Approver{log: logger.WithSource(log, "orchestration/audit")} } // ReviewAndApprove validates the request, checks ctx, writes a structured audit // log (who / what / when / result), and returns the recorded approval with // Allowed set to true. // // Whether an action is actually sensitive and whether it should be granted is // decided by the upper policy layer; this method only records, narrates and // returns the result model. It returns ErrUnknownActor / ErrActionMissing / // ErrExplainMissing for blank inputs and wraps ctx.Err() when ctx is canceled. func (a *Approver) ReviewAndApprove(ctx context.Context, actor, action, describe string) (*Approval, error) { if err := validateApproval(actor, action, describe); err != nil { return nil, err } if err := ctx.Err(); err != nil { return nil, fmt.Errorf("audit: review-and-approve: %w", err) } ap := &Approval{ Actor: actor, Action: action, Description: describe, Allowed: true, Reviewer: reviewerMain, At: time.Now(), } a.logAudit(ap) return ap, nil } // logAudit writes the structured who/what/when/result audit record. func (a *Approver) logAudit(ap *Approval) { if a.log == nil { return } a.log.Info("audit: review-and-approve", slog.String("actor", ap.Actor), slog.String("action", ap.Action), slog.String("explain", ap.Description), slog.Bool("allowed", ap.Allowed), slog.String("reviewer", ap.Reviewer), slog.Time("at", ap.At), ) } // validateApproval rejects blank inputs with the matching sentinel. func validateApproval(actor, action, describe string) error { if strings.TrimSpace(actor) == "" { return fmt.Errorf("%w: actor is blank", ErrUnknownActor) } if strings.TrimSpace(action) == "" { return fmt.Errorf("%w: action is blank", ErrActionMissing) } if strings.TrimSpace(describe) == "" { return fmt.Errorf("%w: explanation is blank", ErrExplainMissing) } return nil }