fix: address verified gadfly P4/#4 findings (audit/budget/persona)
executus CI / test (push) Failing after 1m4s
executus CI / test (push) Failing after 1m4s
Security (all 3 models — HIGH): audit OnTool persisted raw tool args + results verbatim for the very tools the OnStep narration-redaction flags as secret (mcp_call/email_send/http_*) — the args/results are what CARRY the secret, so they landed in skill_run_logs unredacted. Factored the predicate into isSecretTool() (single source of truth) and OnTool now emits args_redacted/result_redacted (+ lengths) for secret tools. Test asserts no secret reaches the log. (persona) webhook_ip_allowlist entries are now CIDR/IP-validated at load (malformed dropped + warned) instead of accepted raw. Contract correctness (glm-5.2 + deepseek) — audit Memory now honors its documented Storage contract: ListChildrenByParent/ListFinishedRunsBefore return oldest-first; WalkParentChain returns root-first and honors MaxParentChainDepth; ListRunsFiltered clamps limit (<=0 or >500 -> 50); ListFinishedRunsBefore with limit<=0 returns none; an explicit RunFilter.Status (incl. "dry_run") matches regardless of IncludeDryRun; LastRunBySkills counts only status=="ok" unless includeFailed. (PurgeOlderThan's FinishedAt key is the SAFE behavior — in-flight runs retained — so the doc was aligned to it, not the impl.) Error-handling: appendLog now uses a bounded context (auditAppendTimeout=3s) so a hung backend can't block the run goroutine on the hot path; Sink.StartRun logs its (still best-effort) failure instead of swallowing it; budget Memory.Get uses RLock (RWMutex); budget package doc fixed (was skillexec's); Check uses the budgetWindow constant, not a duplicated literal. Triaged false-positive: NewNoOpBudget returning BudgetTracker is assignable to run.Budget (identical method sets) — no change needed. Core go.sum still free of host/DB deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -540,6 +541,10 @@ func decodeAgentManifest(data []byte) (*Agent, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Validate the webhook IP allow-list (CIDR or bare IP); drop + warn on
|
||||
// malformed entries so a typo can't silently widen or void the allow-list.
|
||||
allowlist := validateIPAllowlist(m.WebhookIPAllowlist, m.Name)
|
||||
|
||||
ag := &Agent{
|
||||
Name: strings.TrimSpace(m.Name),
|
||||
Description: m.Description,
|
||||
@@ -559,7 +564,7 @@ func decodeAgentManifest(data []byte) (*Agent, error) {
|
||||
LowLevelTools: m.LowLevelTools,
|
||||
PersonalizationSources: m.PersonalizationSources,
|
||||
Schedule: strings.TrimSpace(m.Schedule),
|
||||
WebhookIPAllowlist: m.WebhookIPAllowlist,
|
||||
WebhookIPAllowlist: allowlist,
|
||||
ChatbotChannelFilter: strings.TrimSpace(m.ChatbotChannelFilter),
|
||||
DefaultEmoji: m.DefaultEmoji,
|
||||
StateReactEmoji: m.StateReact,
|
||||
@@ -568,3 +573,27 @@ func decodeAgentManifest(data []byte) (*Agent, error) {
|
||||
}
|
||||
return ag, nil
|
||||
}
|
||||
|
||||
// validateIPAllowlist keeps only entries that parse as a CIDR block or a bare
|
||||
// IP; malformed entries are dropped with a warning (a typo must not silently
|
||||
// widen or void the webhook allow-list). The struct field documents "CIDR
|
||||
// strings", so this enforces it at load time.
|
||||
func validateIPAllowlist(entries []string, agent string) []string {
|
||||
var out []string
|
||||
for _, e := range entries {
|
||||
e = strings.TrimSpace(e)
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(e); err == nil {
|
||||
out = append(out, e)
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(e); ip != nil {
|
||||
out = append(out, e)
|
||||
continue
|
||||
}
|
||||
slog.Warn("agents: dropping malformed webhook_ip_allowlist entry (not a CIDR or IP)", "agent", agent, "entry", e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package persona
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateIPAllowlist(t *testing.T) {
|
||||
in := []string{"10.0.0.0/8", " 192.168.1.5 ", "not-an-ip", "", "2001:db8::/32", "garbage/99"}
|
||||
got := validateIPAllowlist(in, "test")
|
||||
want := map[string]bool{"10.0.0.0/8": true, "192.168.1.5": true, "2001:db8::/32": true}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %d valid entries", got, len(want))
|
||||
}
|
||||
for _, e := range got {
|
||||
if !want[e] {
|
||||
t.Errorf("unexpected entry kept: %q", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user