Files
executus/budget/memory.go
T
steve d82cef46b4
executus CI / test (push) Failing after 1m4s
fix: address verified gadfly P4/#4 findings (audit/budget/persona)
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>
2026-06-27 00:12:19 -04:00

57 lines
1.4 KiB
Go

package budget
import (
"context"
"sync"
"time"
)
// Memory is a zero-dependency in-process BudgetStorage: per-user rolling-window
// usage held in memory (lost on restart). The default behind DBBudget for a
// light host or tests; mort uses its GORM Storage, contrib/store adds SQLite.
type Memory struct {
mu sync.RWMutex
rows map[string]*SkillBudget
}
// NewMemory returns an empty in-memory BudgetStorage.
func NewMemory() *Memory { return &Memory{rows: map[string]*SkillBudget{}} }
var _ BudgetStorage = (*Memory)(nil)
func (m *Memory) Initialize(context.Context) error { return nil }
func (m *Memory) Get(_ context.Context, userID string) (*SkillBudget, error) {
m.mu.RLock()
defer m.mu.RUnlock()
r, ok := m.rows[userID]
if !ok {
return nil, nil
}
cp := *r // copy out so callers can't mutate our row
return &cp, nil
}
func (m *Memory) Add(_ context.Context, userID string, secondsUsed float64, now time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rows[userID]
if !ok {
m.rows[userID] = &SkillBudget{
UserID: userID, WindowStart: now,
SecondsUsed: secondsUsed, RunsCount: 1, UpdatedAt: now,
}
return nil
}
// Roll the window over if it's older than the window length.
if now.Sub(r.WindowStart) >= budgetWindow {
r.WindowStart = now
r.SecondsUsed = 0
r.RunsCount = 0
}
r.SecondsUsed += secondsUsed
r.RunsCount++
r.UpdatedAt = now
return nil
}