mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f297d75f2 | ||
|
|
a1f3256cf6 |
+22
-2
@@ -252,7 +252,7 @@ var targetGuidance = map[string]string{
|
||||
// always final (an approval can still arrive, a lookup can succeed later), so
|
||||
// it does not end with "This operation is forbidden."
|
||||
func TargetBlockMessage(res intercept.TargetResult) string {
|
||||
reason := res.Reason
|
||||
reason := capBlockReason(res.Reason)
|
||||
if res.Host != "" && !strings.Contains(reason, res.Host) {
|
||||
target := res.Host
|
||||
if res.Port != 0 {
|
||||
@@ -277,7 +277,27 @@ func TargetBlockMessage(res intercept.TargetResult) string {
|
||||
// model-facing tool_result carries this framing.
|
||||
func systemBlockMessage(reason string) string {
|
||||
return "[ARTEX platform control, not target defense] This call was intercepted by the platform. " +
|
||||
"Reason: " + reason + ". This operation is forbidden."
|
||||
"Reason: " + capBlockReason(reason) + ". This operation is forbidden."
|
||||
}
|
||||
|
||||
// maxBlockReasonRunes bounds the reason a block message carries back to the
|
||||
// model. Rule messages are operator text and judge or target reasons can embed
|
||||
// command input, so a reason has no natural bound; 4000 runes keeps every
|
||||
// realistic reason whole while one oversized reason cannot flood the context.
|
||||
// Only the model-facing text is capped: Interceptor.Log stores the raw reason.
|
||||
const maxBlockReasonRunes = 4000
|
||||
|
||||
// capBlockReason returns s unchanged when it has at most maxBlockReasonRunes
|
||||
// runes, and otherwise its first maxBlockReasonRunes runes and a marker.
|
||||
func capBlockReason(s string) string {
|
||||
n := 0
|
||||
for i := range s {
|
||||
if n == maxBlockReasonRunes {
|
||||
return s[:i] + " …(truncated)"
|
||||
}
|
||||
n++
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var reBlocked = regexp.MustCompile(`(?i)\b(403|forbidden|waf|blocked|rate.?limit|429|captcha|denied)\b`)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/intercept"
|
||||
@@ -150,4 +151,65 @@ func TestTargetOnlyGuardAppliesBuiltinDenyRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A deny rule with an oversized message reaches the model capped, with the
|
||||
// platform framing and the guidance intact.
|
||||
func TestDenyRuleReasonIsCapped(t *testing.T) {
|
||||
ic := intercept.New(nil)
|
||||
ic.SetRulesForTest([]db.InterceptRule{
|
||||
{ID: 1, Name: "Huge message", Enabled: true, Priority: 50,
|
||||
MatchTarget: "tool_input", MatchType: "string", Action: "deny",
|
||||
Pattern: "whoami", Message: strings.Repeat("日", 20000)},
|
||||
})
|
||||
block, msg := pre(NewWithInterceptor(ic), context.Background(), "Bash", `{"command":"whoami"}`)
|
||||
if !block {
|
||||
t.Fatal("the deny rule did not block")
|
||||
}
|
||||
limit := maxBlockReasonRunes + utf8.RuneCountInString(systemBlockMessage("")+" …(truncated)")
|
||||
if n := utf8.RuneCountInString(msg); n > limit {
|
||||
t.Fatalf("message has %d runes, want at most %d", n, limit)
|
||||
}
|
||||
if !utf8.ValidString(msg) ||
|
||||
!strings.HasPrefix(msg, "[ARTEX platform control, not target defense] This call was intercepted by the platform. Reason: ") ||
|
||||
!strings.HasSuffix(msg, strings.Repeat("日", maxBlockReasonRunes)+" …(truncated). This operation is forbidden.") {
|
||||
t.Errorf("capped message lost its framing or guidance: %q...%q", msg[:120], msg[len(msg)-120:])
|
||||
}
|
||||
}
|
||||
|
||||
// A target block with an oversized reason is capped the same way, and keeps the
|
||||
// target and the guidance of its code.
|
||||
func TestTargetBlockReasonIsCapped(t *testing.T) {
|
||||
res := intercept.TargetResult{Code: intercept.TargetDenied, Host: "app.example.com", Port: 443,
|
||||
Reason: strings.Repeat("é", 20000)}
|
||||
msg := TargetBlockMessage(res)
|
||||
empty := res
|
||||
empty.Reason = ""
|
||||
limit := maxBlockReasonRunes + utf8.RuneCountInString(TargetBlockMessage(empty)+" …(truncated)")
|
||||
if n := utf8.RuneCountInString(msg); n > limit {
|
||||
t.Fatalf("message has %d runes, want at most %d", n, limit)
|
||||
}
|
||||
if !utf8.ValidString(msg) ||
|
||||
!strings.HasPrefix(msg, "[ARTEX platform control, not target defense] TARGET_DENIED: target app.example.com:443: ") ||
|
||||
!strings.HasSuffix(msg, strings.Repeat("é", maxBlockReasonRunes)+" …(truncated). "+targetGuidance[intercept.TargetDenied]) {
|
||||
t.Errorf("capped message lost its target or guidance: %q...%q", msg[:120], msg[len(msg)-120:])
|
||||
}
|
||||
}
|
||||
|
||||
// A reason within the cap passes through byte for byte.
|
||||
func TestShortBlockReasonUnchanged(t *testing.T) {
|
||||
for _, s := range []string{"", "Destructive command rejected", strings.Repeat("日", maxBlockReasonRunes)} {
|
||||
if got := capBlockReason(s); got != s {
|
||||
t.Errorf("capBlockReason changed a reason of %d runes", utf8.RuneCountInString(s))
|
||||
}
|
||||
}
|
||||
if got, want := systemBlockMessage("Destructive command rejected"),
|
||||
"[ARTEX platform control, not target defense] This call was intercepted by the platform. Reason: Destructive command rejected. This operation is forbidden."; got != want {
|
||||
t.Errorf("systemBlockMessage = %q, want %q", got, want)
|
||||
}
|
||||
res := intercept.TargetResult{Code: intercept.TargetDenied, Host: "app.example.com", Port: 443, Reason: "the request was denied"}
|
||||
if got, want := TargetBlockMessage(res),
|
||||
"[ARTEX platform control, not target defense] TARGET_DENIED: target app.example.com:443: the request was denied. "+targetGuidance[intercept.TargetDenied]; got != want {
|
||||
t.Errorf("TargetBlockMessage = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var _ = hook.PreToolUse
|
||||
|
||||
Reference in New Issue
Block a user