d82cef46b4
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>
57 lines
1.4 KiB
Go
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
|
|
}
|