162 lines
6.3 KiB
Go
162 lines
6.3 KiB
Go
package audit
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"log/slog"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
)
|
||
|
||
// Approver 返回的哨兵错误。用 errors.Is 判断它们。
|
||
var (
|
||
// ErrActionMissing 在 action 为空时返回。
|
||
ErrActionMissing = errors.New("audit: action is missing")
|
||
// ErrUnknownActor 在扮演角色的名字为空时返回。本包没有角色注册表,因此空名字是它
|
||
// 唯一能检出的「未知」;某个角色是否真的获授权由上游决定。
|
||
ErrUnknownActor = errors.New("audit: actor is unknown")
|
||
// ErrExplainMissing 在说明为空时返回。
|
||
ErrExplainMissing = errors.New("audit: explanation is missing")
|
||
)
|
||
|
||
// reviewerMain 是执行该审查的主 agent 所记录的名字。
|
||
const reviewerMain = "main"
|
||
|
||
// Approval 是对子代理敏感操作的一次主代理代审的不可变记录。
|
||
type Approval struct {
|
||
// Actor 是扮演角色的名字(角色名)。
|
||
Actor string
|
||
// Action 是被批准的敏感操作。
|
||
Action string
|
||
// Description 是对扮演者想做什么的说明(该操作意味着什么)。它命名为
|
||
// Description 而非 Explain,因为 Explain 方法(见下)返回的是前端提示短句。
|
||
Description string
|
||
// Subagent 报告这次调用是否由子代理发起(口径 A 的唯一判定对象)。false 表示
|
||
// 这是主会话自身的调用:不加判定、直接放行。
|
||
Subagent bool
|
||
// Task 是被判定时所依据的「派给该子代理的任务目标」(已截断);非子代理调用为空。
|
||
Task string
|
||
// Reason 是判定给出的可读理由:判「越界」或判不出来(fail-closed)时说明原因;
|
||
// 主会话调用与判定放行时为空。它会进审计与前端小字,绝不静默拒绝。
|
||
Reason string
|
||
// Allowed 报告该操作是否获准。
|
||
Allowed bool
|
||
// Reviewer 是执行该审查的主 agent。
|
||
Reviewer string
|
||
// At 是代审发生的时刻。
|
||
At time.Time
|
||
}
|
||
|
||
// Explain 返回描述审查结果的前端提示短句。获准时是
|
||
// "「<actor> 想要干什么,我审查后批准了」";被拒时用 "我审查后拒绝了"。<actor> 处会
|
||
// 代入 Actor。
|
||
func (a Approval) Explain() string {
|
||
if a.Allowed {
|
||
return "「" + a.Actor + " 想要干什么,我审查后批准了」"
|
||
}
|
||
return "「" + a.Actor + " 想要干什么,我审查后拒绝了」"
|
||
}
|
||
|
||
// Approver 记录主代理代审事件及其审计日志。
|
||
type Approver struct {
|
||
log *slog.Logger
|
||
// origin 回答「这次调用是否由子代理发起」(口径 A 的开关);nil 表示不做实质判定。
|
||
origin SubagentOrigin
|
||
// judge 做实质判定;nil 而 origin 说「是子代理发起」时一律拒绝(fail-closed)。
|
||
judge CallJudge
|
||
// judgeTimeout 是单次判定的时限(默认 judgeTimeout)。
|
||
judgeTimeout time.Duration
|
||
}
|
||
|
||
// NewApprover 构建一个经 log 写入审计日志的 Approver。该 logger 会被标注来源
|
||
// "orchestration/audit"。允许传入 nil logger,此时 approver 静默记录(无日志输出)。
|
||
//
|
||
// 组合根经 WithSubagentOrigin / WithCallJudge 装配口径 A 的实质判定;两者都不装配时
|
||
// 保持既有行为(每次代审都记一条放行记录)。
|
||
func NewApprover(log *slog.Logger, opts ...ApproverOption) *Approver {
|
||
a := &Approver{
|
||
log: logger.WithSource(log, "orchestration/audit"),
|
||
judgeTimeout: judgeTimeout,
|
||
}
|
||
for _, opt := range opts {
|
||
if opt != nil {
|
||
opt(a)
|
||
}
|
||
}
|
||
// 装配了判定器却没有来源解析 = 判定永不触发。这属于接线错误,必须显式告警,
|
||
// 否则「判定器已装配」会给人一个假的安心(实际一次都没跑)。
|
||
if a.judge != nil && a.origin == nil && a.log != nil {
|
||
a.log.Warn("代审判定器已装配但没有子代理来源解析,判定不会触发",
|
||
"error", ErrNoJudgeOrigin)
|
||
}
|
||
return a
|
||
}
|
||
|
||
// ReviewAndApprove 校验请求、检查 ctx、实施口径 A 的实质判定,写入结构化审计日志
|
||
// (谁 / 什么 / 何时 / 结果 / 理由),并返回记录下来的 approval。
|
||
//
|
||
// 判定的边界(主人 2026-10-05 裁决):
|
||
// - **只对子代理发起的敏感调用**做判定,判「这次调用是否与派给它的任务一致」;
|
||
// 主会话自身的调用不加判定、直接放行(否则主人自己显式要求的操作会被拦死)。
|
||
// - **fail-closed**:未装配判定器 / 判定失败 / 超时 / 判不出来 ⇒ 不放行,并在
|
||
// 返回的 Approval.Reason 里如实写明原因。
|
||
// - 判定只**收紧**、绝不**放宽**:某项操作是否真的敏感由上层策略(capability/restrict
|
||
// 的结构化判据)先决,本方法及其判定器都不判身份、不判权限。
|
||
//
|
||
// 入参为空时返回 ErrUnknownActor / ErrActionMissing / ErrExplainMissing,ctx 被取消时
|
||
// 包装 ctx.Err()。
|
||
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.judgeCall(ctx, ap)
|
||
a.logAudit(ap)
|
||
return ap, nil
|
||
}
|
||
|
||
// logAudit 写入结构化的「谁/什么/何时/结果/理由」审计记录。它只记判定理由与是否
|
||
// 子代理发起,绝不记判定问句里的参数摘要(参数里可能有秘密)。
|
||
func (a *Approver) logAudit(ap *Approval) {
|
||
if a == nil || 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("subagent", ap.Subagent),
|
||
slog.String("reason", ap.Reason),
|
||
slog.Bool("allowed", ap.Allowed),
|
||
slog.String("reviewer", ap.Reviewer),
|
||
slog.Time("at", ap.At),
|
||
)
|
||
}
|
||
|
||
// validateApproval 以对应的哨兵错误拒绝空入参。
|
||
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
|
||
}
|