feat: 拦截规则 allow/deny 命中也进审批记录(三种 action 全可观测)
此前只有 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 过滤仅用于审批按钮区。
This commit is contained in:
1 parent
05eb7d2174
commit
6348ec9d21
3 files changed
+40
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user