feat(security): 代审口径 A 落地(仅子代理发起的敏感调用做实质判定 + fail-closed + 理由进审计与小字)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 12:12:31 +08:00
1 parent fa850f9599
commit 20160c66d8
13 files changed
+1479 -83

No files matched your search

@@ -34,6 +34,14 @@ type Approval struct {
// Description 是对扮演者想做什么的说明(该操作意味着什么)。它命名为
// Description 而非 Explain,因为 Explain 方法(见下)返回的是前端提示短句。
Description string
// Subagent 报告这次调用是否由子代理发起(口径 A 的唯一判定对象)。false 表示
// 这是主会话自身的调用:不加判定、直接放行。
Subagent bool
// Task 是被判定时所依据的「派给该子代理的任务目标」(已截断);非子代理调用为空。
Task string
// Reason 是判定给出的可读理由:判「越界」或判不出来(fail-closed)时说明原因;
// 主会话调用与判定放行时为空。它会进审计与前端小字,绝不静默拒绝。
Reason string
// Allowed 报告该操作是否获准。
Allowed bool
// Reviewer 是执行该审查的主 agent。
@@ -55,18 +63,49 @@ func (a Approval) Explain() string {
// 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 静默记录(无日志输出)。
func NewApprover(log *slog.Logger) *Approver {
return &Approver{log: logger.WithSource(log, "orchestration/audit")}
//
// 组合根经 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、写入结构化审计日志(谁 / 什么 / 何时 / 结果),
// 并返回记录下来的 approval,其 Allowed 置为 true。
// 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) {
@@ -84,19 +123,23 @@ func (a *Approver) ReviewAndApprove(ctx context.Context, actor, action, describe
Reviewer: reviewerMain,
At: time.Now(),
}
a.judgeCall(ctx, ap)
a.logAudit(ap)
return ap, nil
}
// logAudit 写入结构化的「谁/什么/何时/结果」审计记录。
// logAudit 写入结构化的「谁/什么/何时/结果/理由」审计记录。它只记判定理由与是否
// 子代理发起,绝不记判定问句里的参数摘要(参数里可能有秘密)。
func (a *Approver) logAudit(ap *Approval) {
if a.log == nil {
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),