mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
This commit introduces a comprehensive Starlark scripting system that enables custom automation with access to all subdaemon RPCs (lnd, loop, pool, faraday, taproot-assets). Scripts use native LND macaroons for permission enforcement. Key features: - Starlark execution engine with sandboxed resource limits - Standard builtins: print, log, sleep, now, json_encode/decode - HTTP GET requests with URL allowlisting - Persistent KV store with bucket-based permissions - LND event subscriptions for long-running daemon scripts - Script CRUD operations with macaroon baking - Execution history and running script tracking - Complete CLI commands (litcli scripts ...) Database schema: - scripts: Store script definitions with macaroon permissions - script_executions: Audit trail for script runs - script_kv_store: Persistent key-value storage for scripts - running_scripts: Track currently running scripts Security model: - Each script has an LND macaroon baked with specific permissions - RPC calls from scripts include this macaroon in the header - LND/subdaemons validate permissions natively - URL and bucket allowlists validated at runtime
199 lines
4.3 KiB
Go
199 lines
4.3 KiB
Go
package scripting
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// DefaultMaxMemoryBytes is the default maximum memory a script can use
|
|
// (100MB).
|
|
DefaultMaxMemoryBytes = 100 * 1024 * 1024
|
|
|
|
// DefaultTimeoutSecs is the default execution timeout for scripts.
|
|
// 0 means no timeout (for long-running scripts).
|
|
DefaultTimeoutSecs = 0
|
|
|
|
// MaxSleepDuration is the maximum time a script can sleep in a single
|
|
// call.
|
|
MaxSleepDuration = 60 * time.Second
|
|
)
|
|
|
|
// ResourceLimits defines the resource constraints for script execution.
|
|
type ResourceLimits struct {
|
|
// MaxMemoryBytes is the maximum memory the script can allocate.
|
|
MaxMemoryBytes uint64
|
|
|
|
// TimeoutSecs is the execution timeout in seconds. 0 means no timeout.
|
|
TimeoutSecs uint32
|
|
}
|
|
|
|
// DefaultResourceLimits returns the default resource limits.
|
|
func DefaultResourceLimits() ResourceLimits {
|
|
return ResourceLimits{
|
|
MaxMemoryBytes: DefaultMaxMemoryBytes,
|
|
TimeoutSecs: DefaultTimeoutSecs,
|
|
}
|
|
}
|
|
|
|
// Sandbox enforces resource limits during script execution.
|
|
type Sandbox struct {
|
|
limits ResourceLimits
|
|
|
|
// ctx is the execution context with cancellation.
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// memoryUsed tracks approximate memory usage.
|
|
memoryUsed atomic.Int64
|
|
|
|
// startTime is when execution started.
|
|
startTime time.Time
|
|
|
|
// stopped indicates if the sandbox has been stopped.
|
|
stopped atomic.Bool
|
|
|
|
// mu protects concurrent access.
|
|
mu sync.RWMutex
|
|
|
|
// callbacks for cleanup when sandbox is stopped.
|
|
cleanupFuncs []func()
|
|
}
|
|
|
|
// NewSandbox creates a new sandbox with the given limits.
|
|
func NewSandbox(ctx context.Context, limits ResourceLimits) *Sandbox {
|
|
sandboxCtx, cancel := context.WithCancel(ctx)
|
|
|
|
s := &Sandbox{
|
|
limits: limits,
|
|
ctx: sandboxCtx,
|
|
cancel: cancel,
|
|
startTime: time.Now(),
|
|
}
|
|
|
|
// Set up timeout if configured.
|
|
if limits.TimeoutSecs > 0 {
|
|
go func() {
|
|
timeout := time.Duration(limits.TimeoutSecs) * time.Second
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-timer.C:
|
|
log.Infof("Script execution timed out after %v", timeout)
|
|
s.Stop()
|
|
case <-sandboxCtx.Done():
|
|
// Context cancelled, nothing to do.
|
|
}
|
|
}()
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Context returns the sandbox context.
|
|
func (s *Sandbox) Context() context.Context {
|
|
return s.ctx
|
|
}
|
|
|
|
// Stop cancels the sandbox context and runs cleanup functions.
|
|
func (s *Sandbox) Stop() {
|
|
if s.stopped.CompareAndSwap(false, true) {
|
|
s.cancel()
|
|
|
|
s.mu.Lock()
|
|
cleanups := s.cleanupFuncs
|
|
s.cleanupFuncs = nil
|
|
s.mu.Unlock()
|
|
|
|
for _, cleanup := range cleanups {
|
|
cleanup()
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsStopped returns whether the sandbox has been stopped.
|
|
func (s *Sandbox) IsStopped() bool {
|
|
return s.stopped.Load()
|
|
}
|
|
|
|
// RegisterCleanup adds a cleanup function to be called when the sandbox stops.
|
|
func (s *Sandbox) RegisterCleanup(fn func()) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.stopped.Load() {
|
|
// Already stopped, run cleanup immediately.
|
|
fn()
|
|
return
|
|
}
|
|
|
|
s.cleanupFuncs = append(s.cleanupFuncs, fn)
|
|
}
|
|
|
|
// CheckMemory checks if allocating the given amount of memory is allowed.
|
|
func (s *Sandbox) CheckMemory(bytes int64) error {
|
|
if s.limits.MaxMemoryBytes == 0 {
|
|
return nil
|
|
}
|
|
|
|
newUsage := s.memoryUsed.Add(bytes)
|
|
if newUsage > int64(s.limits.MaxMemoryBytes) {
|
|
s.memoryUsed.Add(-bytes)
|
|
return fmt.Errorf("memory limit exceeded: %d > %d",
|
|
newUsage, s.limits.MaxMemoryBytes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReleaseMemory releases previously allocated memory.
|
|
func (s *Sandbox) ReleaseMemory(bytes int64) {
|
|
s.memoryUsed.Add(-bytes)
|
|
}
|
|
|
|
// MemoryUsed returns the current memory usage.
|
|
func (s *Sandbox) MemoryUsed() int64 {
|
|
return s.memoryUsed.Load()
|
|
}
|
|
|
|
// ElapsedTime returns how long the script has been running.
|
|
func (s *Sandbox) ElapsedTime() time.Duration {
|
|
return time.Since(s.startTime)
|
|
}
|
|
|
|
// Sleep pauses execution for the given duration, respecting context
|
|
// cancellation.
|
|
func (s *Sandbox) Sleep(d time.Duration) error {
|
|
if d > MaxSleepDuration {
|
|
return fmt.Errorf("sleep duration %v exceeds maximum %v",
|
|
d, MaxSleepDuration)
|
|
}
|
|
|
|
if d <= 0 {
|
|
return nil
|
|
}
|
|
|
|
timer := time.NewTimer(d)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-timer.C:
|
|
return nil
|
|
case <-s.ctx.Done():
|
|
return s.ctx.Err()
|
|
}
|
|
}
|
|
|
|
// CheckContext checks if the context has been cancelled.
|
|
func (s *Sandbox) CheckContext() error {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return s.ctx.Err()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|