lightning-terminal/scripting/kvstore.go
sputn1ck 5cf25ca624 scripting: add Starlark scripting system for LiT automation
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
2026-01-30 23:28:52 +01:00

151 lines
3.7 KiB
Go

package scripting
import (
"context"
"strings"
"time"
)
// KVStoreDB defines the interface for the KV store database operations.
type KVStoreDB interface {
// KVGet retrieves a value from the store.
KVGet(ctx context.Context, bucket, key string) ([]byte, error)
// KVPut stores a value in the store.
KVPut(ctx context.Context, bucket, key string, value []byte) error
// KVDelete removes a value from the store.
KVDelete(ctx context.Context, bucket, key string) error
// KVList returns all keys in a bucket matching the prefix.
KVList(ctx context.Context, bucket, prefix string) ([]string, error)
// KVDeleteBucket deletes all keys in a bucket.
KVDeleteBucket(ctx context.Context, bucket string) error
}
// SQLKVStore implements KVStore using the SQL database.
type SQLKVStore struct {
db KVStoreDB
}
// NewSQLKVStore creates a new SQL-backed KV store.
func NewSQLKVStore(db KVStoreDB) *SQLKVStore {
return &SQLKVStore{db: db}
}
// Get retrieves a value from the store.
func (s *SQLKVStore) Get(bucket, key string) ([]byte, bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
value, err := s.db.KVGet(ctx, bucket, key)
if err != nil {
// Check if it's a "not found" error.
if strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "no rows") {
return nil, false, nil
}
return nil, false, err
}
if value == nil {
return nil, false, nil
}
return value, true, nil
}
// Put stores a value in the store.
func (s *SQLKVStore) Put(bucket, key string, value []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return s.db.KVPut(ctx, bucket, key, value)
}
// Delete removes a value from the store.
func (s *SQLKVStore) Delete(bucket, key string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return s.db.KVDelete(ctx, bucket, key)
}
// List returns all keys in a bucket matching the prefix.
func (s *SQLKVStore) List(bucket, prefix string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return s.db.KVList(ctx, bucket, prefix)
}
// DeleteBucket deletes all keys in a bucket.
func (s *SQLKVStore) DeleteBucket(bucket string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return s.db.KVDeleteBucket(ctx, bucket)
}
// InMemoryKVStore is a simple in-memory KV store for testing.
type InMemoryKVStore struct {
data map[string]map[string][]byte
}
// NewInMemoryKVStore creates a new in-memory KV store.
func NewInMemoryKVStore() *InMemoryKVStore {
return &InMemoryKVStore{
data: make(map[string]map[string][]byte),
}
}
// Get retrieves a value from the store.
func (s *InMemoryKVStore) Get(bucket, key string) ([]byte, bool, error) {
bucketData, ok := s.data[bucket]
if !ok {
return nil, false, nil
}
value, ok := bucketData[key]
if !ok {
return nil, false, nil
}
return value, true, nil
}
// Put stores a value in the store.
func (s *InMemoryKVStore) Put(bucket, key string, value []byte) error {
if _, ok := s.data[bucket]; !ok {
s.data[bucket] = make(map[string][]byte)
}
s.data[bucket][key] = value
return nil
}
// Delete removes a value from the store.
func (s *InMemoryKVStore) Delete(bucket, key string) error {
if bucketData, ok := s.data[bucket]; ok {
delete(bucketData, key)
}
return nil
}
// List returns all keys in a bucket matching the prefix.
func (s *InMemoryKVStore) List(bucket, prefix string) ([]string, error) {
bucketData, ok := s.data[bucket]
if !ok {
return nil, nil
}
var keys []string
for key := range bucketData {
if prefix == "" || strings.HasPrefix(key, prefix) {
keys = append(keys, key)
}
}
return keys, nil
}