From 6348ec9d21d2a22f47d3a980bc0fbde6064e70c1 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:03:56 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=A6=E6=88=AA=E8=A7=84=E5=88=99=20?= =?UTF-8?q?allow/deny=20=E5=91=BD=E4=B8=AD=E4=B9=9F=E8=BF=9B=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E8=AE=B0=E5=BD=95(=E4=B8=89=E7=A7=8D=20action=20?= =?UTF-8?q?=E5=85=A8=E5=8F=AF=E8=A7=82=E6=B5=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前只有 ask 命中会写 intercept_pending(审批记录);deny 直接拦下、allow 直接放行, 历史里查不到。现让三种 action 命中都留痕,便于观测: - db: CreateDecidedIntercept —— 一条 insert 直接以最终状态(allowed/denied)落库并盖 decided_at(复用 intercept_pending 现有合法状态,无需 schema 迁移)。 - intercept: Interceptor.Log —— 非阻塞、best-effort 记录一次规则命中(DB 出错不影响 工具调用结果)。 - guard: deny 分支记 denied、显式 allow 规则命中记 allowed;无规则命中的放行不记, 避免全量刷屏。 前端零改动:审批记录/历史页(interceptHistory=全状态、interceptTask=全状态)本就渲染 allowed/denied/pending;pending 过滤仅用于审批按钮区。 --- db/intercept.go | 25 +++++++++++++++++++++++++ guard/guard.go | 4 ++++ intercept/intercept.go | 11 +++++++++++ 3 files changed, 40 insertions(+) diff --git a/db/intercept.go b/db/intercept.go index 0e6f2c7..cc7a902 100644 --- a/db/intercept.go +++ b/db/intercept.go @@ -132,6 +132,31 @@ func (d *DB) DecideInterceptPending(id int64, status string) error { return err } +// CreateDecidedIntercept inserts an intercept_pending row ALREADY in a final state +// (status = 'allowed' | 'denied'), decided_at stamped now. Used to log allow/deny +// rule matches for observability — they don't block and need no user action, so unlike +// CreateInterceptPending (which starts 'pending') this records the outcome directly. +func (d *DB) CreateDecidedIntercept(ruleID, convID int64, taskID, agentName, toolName string, input []byte, status string) (int64, error) { + raw := json.RawMessage(input) + if len(raw) == 0 { + raw = json.RawMessage("{}") + } + var convIDPtr *int64 + if convID != 0 { + convIDPtr = &convID + } + var taskIDPtr *string + if taskID != "" { + taskIDPtr = &taskID + } + var id int64 + err := d.QueryRow(` +INSERT INTO intercept_pending(rule_id, conversation_id, task_id, agent_name, tool_name, tool_input, status, decided_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING id`, + ruleID, convIDPtr, taskIDPtr, agentName, toolName, raw, status).Scan(&id) + return id, err +} + const interceptPendingCols = `id, rule_id, conversation_id, task_id, agent_name, tool_name, tool_input, status, decided_at, created_at` func scanInterceptPending(s interface{ Scan(...any) error }, p *InterceptPending) error { diff --git a/guard/guard.go b/guard/guard.go index 6f3d1fd..b99ac19 100644 --- a/guard/guard.go +++ b/guard/guard.go @@ -93,8 +93,12 @@ func (g *Guard) applyIntercept(ctx context.Context, ev hook.Event) hook.Result { } switch dec.Action { case "deny": + // 观测:deny 命中不阻塞审批,直接记一条 denied(历史/任务拦截页可见)。 + g.interceptor.Log(ctx, intercept.ConvIDFromContext(ctx), dec, ev.ToolName, ev.Input, "denied") return g.block(ev.ToolName, dec.Message, "") case "allow": + // 观测:显式 allow 规则命中记一条 allowed(无规则命中的放行不记,避免全量刷屏)。 + g.interceptor.Log(ctx, intercept.ConvIDFromContext(ctx), dec, ev.ToolName, ev.Input, "allowed") return hook.Result{} case "ask": // If the worker context is already cancelled (task stopped / killed), block diff --git a/intercept/intercept.go b/intercept/intercept.go index 8e71c33..f69fea4 100644 --- a/intercept/intercept.go +++ b/intercept/intercept.go @@ -329,6 +329,17 @@ func defaultMessage(action, name string) string { } } +// Log records an allow/deny rule match into intercept_pending as an ALREADY-decided +// row (status = "allowed" | "denied"), for observability. Unlike HandleAsk it does NOT +// block and needs no user action — it just makes every rule hit visible on the history +// page (GET /api/intercept/history) and the task's intercept list. Best-effort: a DB +// error is swallowed so logging never changes the tool call's outcome. The pending list +// (status='pending') is unaffected, so it still shows only asks awaiting a decision. +func (i *Interceptor) Log(ctx context.Context, convID int64, dec Decision, toolName string, input []byte, status string) { + taskID, agentName := taskInfoFromCtx(ctx) + _, _ = i.db.CreateDecidedIntercept(dec.RuleID, convID, taskID, agentName, toolName, input, status) +} + // HandleAsk creates a pending approval record and blocks until the user decides // (via /api/intercept/pending/{id}/decide) or the per-rule timeout elapses. // Returns true if the user approved.