Files
Agent-Native/runtime/guardrails/guardrails_test.go
weichen 948ca36ca2
ci / vet-and-test (pull_request) Successful in 47s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Add AgentSpec, three guardrail ports, subagent events, and ask passthrough
Why: N1 increment three (WP1/WP4) - agents need a canonical spec
format with plugin restrictions, hosts need typed input/output/tool
guardrails, delegation needs lifecycle events, and squad adapters
need to pass the permission ask port through.

Evidence: go vet clean; go test ./... 26 packages ok, including new
agentspec (12 tests), guardrails (5), loop_guardrails (5), and
subagent hook tests (3).

Impact: new runtime/agentspec and runtime/guardrails packages; loop
consumes UserPromptSubmit outcomes (deny skips the loop, modify
appends the rewrite); subagent gains RegisterWithHooks emitting
SubagentStart/Stop around delegation; GateOptions gains PermissionAsk
forwarded into the kernel config. All additions are opt-in: nil
ports preserve prior behavior exactly.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP1/WP4
2026-10-07 05:04:58 +00:00

90 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package guardrails
import (
"context"
"testing"
"agent-native/runtime/hooks"
)
func TestToolGuardrailMergesOnPreToolUse(t *testing.T) {
bus := hooks.New()
Register(bus, Set{
Tool: func(ctx context.Context, name, args string) Verdict {
if name == "run_shell" {
return Verdict{Decision: hooks.Deny, Reason: "shell 被护栏禁用"}
}
return Verdict{}
},
}, Scope{})
out := bus.Emit(hooks.Call{Event: hooks.PreToolUse, Tool: "run_shell"})
if out.Decision != hooks.Deny || out.Reason == "" {
t.Fatalf("out = %+v", out)
}
out = bus.Emit(hooks.Call{Event: hooks.PreToolUse, Tool: "read_file"})
if out.Decision != "" {
t.Fatalf("read_file out = %+v", out)
}
}
func TestToolGuardrailModifyRewritesArgs(t *testing.T) {
bus := hooks.New()
Register(bus, Set{
Tool: func(ctx context.Context, name, args string) Verdict {
return Verdict{Decision: hooks.Modify, Replacement: `{"path":"safe.txt"}`}
},
}, Scope{})
out := bus.Emit(hooks.Call{Event: hooks.PreToolUse, Tool: "write_file"})
if out.Decision != hooks.Modify || out.Args != `{"path":"safe.txt"}` {
t.Fatalf("out = %+v", out)
}
}
func TestOutputGuardrailDenyBecomesContinuation(t *testing.T) {
bus := hooks.New()
Register(bus, Set{
Output: func(ctx context.Context, text string) Verdict {
return Verdict{Decision: hooks.Deny, Reason: "缺少来源标注"}
},
}, Scope{Final: true})
out := bus.Emit(hooks.Call{Event: hooks.Stop, LastText: "结论"})
// Output deny 必须表达为续跑(Block),而不是把 run 掐死。
if out.Decision != hooks.Block || out.Reason != "缺少来源标注" {
t.Fatalf("out = %+v", out)
}
}
func TestScopeGatesInputAndOutput(t *testing.T) {
fired := map[string]bool{}
mk := func() Set {
return Set{
Input: func(ctx context.Context, text string) Verdict { fired["input"] = true; return Verdict{} },
Output: func(ctx context.Context, text string) Verdict { fired["output"] = true; return Verdict{} },
}
}
// 子代理 run(Entry/Final 都为 false):Input/Output 不注册。
bus := hooks.New()
Register(bus, mk(), Scope{})
bus.Emit(hooks.Call{Event: hooks.UserPromptSubmit, LastText: "x"})
bus.Emit(hooks.Call{Event: hooks.Stop, LastText: "y"})
if fired["input"] || fired["output"] {
t.Fatalf("sub-run scope fired: %v", fired)
}
// 入口且最终:两层都触发。
bus2 := hooks.New()
Register(bus2, mk(), Scope{Entry: true, Final: true})
bus2.Emit(hooks.Call{Event: hooks.UserPromptSubmit, LastText: "x"})
bus2.Emit(hooks.Call{Event: hooks.Stop, LastText: "y"})
if !fired["input"] || !fired["output"] {
t.Fatalf("entry/final scope did not fire: %v", fired)
}
}
func TestNilBusIsNoop(t *testing.T) {
Register(nil, Set{
Input: func(ctx context.Context, text string) Verdict { return Verdict{Decision: hooks.Deny} },
Output: func(ctx context.Context, text string) Verdict { return Verdict{Decision: hooks.Deny} },
Tool: func(ctx context.Context, name, args string) Verdict { return Verdict{Decision: hooks.Deny} },
}, Scope{Entry: true, Final: true})
}