mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 17:04:21 +00:00
8d37dc45ea
Ship Group A (bug fixes) + Group B (pipeline enhancements) from plans/260410-1009-openclaw-ts-feature-port — five changes that share cmd/ wiring and pipeline plumbing so they commit as a unit. cache: InMemoryCache now supports periodic sweep + max-size cap via variadic options. PermissionCache wires 60s sweep + 10k entry cap + Close() hook in gateway shutdown so long-running gateways don't leak per-user permission entries. Backward compatible for zero-arg callers. store: ContactCollector.seen cache key now includes tenant_id + channel_ instance so the same sender in different tenants (or different bot instances in the same tenant) no longer silently skip upserts against each other. Zero-tenant (Desktop) behaviour preserved. pipeline: EffectiveContextWindow is resolved once per run in ContextStage via a ResolveContextWindow callback (backed by providers.ModelRegistry) so PruneStage bills history against the actual model window instead of a stale static config. Nil resolver / unknown model fall back to Config.ContextWindow for backward compatibility. Locked to the model observed at context build time to prevent mid-run budget drift. pipeline: PipelineConfig.ReserveTokens carves out an optional safety buffer subtracted from the history budget so compaction fires slightly before the hard limit — protects against provider over-delivery and token counter drift on streaming responses. Zero (default) preserves legacy budget math. agent: ModelRegistry flows gateway → ResolverDeps → LoopConfig → Loop → pipeline adapter so resolver can look up per-model capabilities at run time without re-touching gateway internals. 20 regression tests across cache, contact collector, and pipeline cover the critical paths: cross-tenant isolation, cache sweep + eviction + Close idempotency, per-model window override + fallback, and reserve token buffer behaviour. All passing with -race on both go build ./... and go build -tags sqliteonly ./....
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
)
|
|
|
|
// agentAccessEntry caches agent access check results.
|
|
type agentAccessEntry struct {
|
|
Allowed bool
|
|
Role string
|
|
}
|
|
|
|
// PermissionCache provides short-TTL caching for hot permission lookups.
|
|
// Uses InMemoryCache[V] caches with pubsub invalidation.
|
|
type PermissionCache struct {
|
|
tenantRole *InMemoryCache[string]
|
|
agentAccess *InMemoryCache[agentAccessEntry]
|
|
teamAccess *InMemoryCache[bool]
|
|
}
|
|
|
|
// permissionCacheSweepInterval and permissionCacheMaxSize bound background
|
|
// growth of per-user cache entries. Without these, long-running gateways with
|
|
// many distinct users would accumulate unbounded entries (tenant_roles, agent
|
|
// access, team access) even with a 30s TTL — lazy eviction only fires on Get,
|
|
// so entries for disconnected users never get reclaimed.
|
|
const (
|
|
permissionCacheSweepInterval = 60 * time.Second
|
|
permissionCacheMaxSize = 10_000
|
|
)
|
|
|
|
// NewPermissionCache creates a new permission cache with periodic sweep
|
|
// goroutines for all three inner caches. Call Close() on gateway shutdown to
|
|
// stop the sweep goroutines.
|
|
func NewPermissionCache() *PermissionCache {
|
|
return &PermissionCache{
|
|
tenantRole: NewInMemoryCache[string](
|
|
WithSweepInterval[string](permissionCacheSweepInterval),
|
|
WithMaxSize[string](permissionCacheMaxSize),
|
|
),
|
|
agentAccess: NewInMemoryCache[agentAccessEntry](
|
|
WithSweepInterval[agentAccessEntry](permissionCacheSweepInterval),
|
|
WithMaxSize[agentAccessEntry](permissionCacheMaxSize),
|
|
),
|
|
teamAccess: NewInMemoryCache[bool](
|
|
WithSweepInterval[bool](permissionCacheSweepInterval),
|
|
WithMaxSize[bool](permissionCacheMaxSize),
|
|
),
|
|
}
|
|
}
|
|
|
|
// Close stops all background sweep goroutines. Safe to call multiple times.
|
|
func (pc *PermissionCache) Close() {
|
|
pc.tenantRole.Close()
|
|
pc.agentAccess.Close()
|
|
pc.teamAccess.Close()
|
|
}
|
|
|
|
const (
|
|
tenantRoleTTL = 30 * time.Second
|
|
agentAccessTTL = 30 * time.Second
|
|
teamAccessTTL = 30 * time.Second
|
|
)
|
|
|
|
// --- Tenant Role ---
|
|
|
|
func (pc *PermissionCache) GetTenantRole(ctx context.Context, tenantID uuid.UUID, userID string) (string, bool) {
|
|
return pc.tenantRole.Get(ctx, tenantID.String()+":"+userID)
|
|
}
|
|
|
|
func (pc *PermissionCache) SetTenantRole(ctx context.Context, tenantID uuid.UUID, userID, role string) {
|
|
pc.tenantRole.Set(ctx, tenantID.String()+":"+userID, role, tenantRoleTTL)
|
|
}
|
|
|
|
// --- Agent Access ---
|
|
|
|
func (pc *PermissionCache) GetAgentAccess(ctx context.Context, agentID uuid.UUID, userID string) (bool, string, bool) {
|
|
entry, ok := pc.agentAccess.Get(ctx, agentID.String()+":"+userID)
|
|
if !ok {
|
|
return false, "", false
|
|
}
|
|
return entry.Allowed, entry.Role, true
|
|
}
|
|
|
|
func (pc *PermissionCache) SetAgentAccess(ctx context.Context, agentID uuid.UUID, userID string, allowed bool, role string) {
|
|
pc.agentAccess.Set(ctx, agentID.String()+":"+userID, agentAccessEntry{Allowed: allowed, Role: role}, agentAccessTTL)
|
|
}
|
|
|
|
// --- Team Access ---
|
|
|
|
func (pc *PermissionCache) GetTeamAccess(ctx context.Context, teamID uuid.UUID, userID string) (bool, bool) {
|
|
return pc.teamAccess.Get(ctx, teamID.String()+":"+userID)
|
|
}
|
|
|
|
func (pc *PermissionCache) SetTeamAccess(ctx context.Context, teamID uuid.UUID, userID string, allowed bool) {
|
|
pc.teamAccess.Set(ctx, teamID.String()+":"+userID, allowed, teamAccessTTL)
|
|
}
|
|
|
|
// --- Invalidation ---
|
|
|
|
// HandleInvalidation processes a cache invalidation event from the bus.
|
|
func (pc *PermissionCache) HandleInvalidation(p bus.CacheInvalidatePayload) {
|
|
slog.Debug("perm_cache.invalidated", "kind", string(p.Kind), "key", p.Key)
|
|
ctx := context.Background()
|
|
switch p.Kind {
|
|
case bus.CacheKindTenantUsers:
|
|
// Key is userID — invalidate all tenant roles.
|
|
// Can't efficiently delete all tenantRole entries for a user by prefix,
|
|
// so clear all tenant roles (short TTL makes this acceptable).
|
|
pc.tenantRole.Clear(ctx)
|
|
case bus.CacheKindAgentAccess:
|
|
// Key is agentID — delete all access entries for this agent.
|
|
if p.Key != "" {
|
|
pc.agentAccess.DeleteByPrefix(ctx, p.Key+":")
|
|
} else {
|
|
pc.agentAccess.Clear(ctx)
|
|
}
|
|
case bus.CacheKindTeamAccess:
|
|
// Key is teamID — delete all access entries for this team.
|
|
if p.Key != "" {
|
|
pc.teamAccess.DeleteByPrefix(ctx, p.Key+":")
|
|
} else {
|
|
pc.teamAccess.Clear(ctx)
|
|
}
|
|
}
|
|
}
|