lightning-terminal/scripting/builtins_kv.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

226 lines
5.5 KiB
Go

package scripting
import (
"fmt"
"go.starlark.net/starlark"
)
// KVStore defines the interface for the key-value store.
type KVStore interface {
// Get retrieves a value from the store.
Get(bucket, key string) ([]byte, bool, error)
// Put stores a value in the store.
Put(bucket, key string, value []byte) error
// Delete removes a value from the store.
Delete(bucket, key string) error
// List returns all keys in a bucket matching the prefix.
List(bucket, prefix string) ([]string, error)
}
// registerKVBuiltins adds KV store builtin functions.
func (e *Engine) registerKVBuiltins(predeclared starlark.StringDict) {
predeclared["kv_get"] = starlark.NewBuiltin("kv_get", e.builtinKVGet)
predeclared["kv_put"] = starlark.NewBuiltin("kv_put", e.builtinKVPut)
predeclared["kv_delete"] = starlark.NewBuiltin("kv_delete", e.builtinKVDelete)
predeclared["kv_list"] = starlark.NewBuiltin("kv_list", e.builtinKVList)
}
// resolveBucket returns the bucket to use, defaulting to the script's own
// bucket if not specified.
func (e *Engine) resolveBucket(bucket string) (string, error) {
if bucket == "" {
return e.scriptName, nil
}
// Check if the script is allowed to access this bucket.
if bucket != e.scriptName && !e.isBucketAllowed(bucket) {
return "", fmt.Errorf("bucket '%s' not in allowlist", bucket)
}
return bucket, nil
}
// isBucketAllowed checks if a bucket is in the script's allowed buckets.
func (e *Engine) isBucketAllowed(bucket string) bool {
for _, allowed := range e.allowedBuckets {
if allowed == bucket {
return true
}
}
return false
}
// builtinKVGet implements kv_get(key, bucket=None).
func (e *Engine) builtinKVGet(thread *starlark.Thread, fn *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key string
var bucket string
if err := starlark.UnpackArgs("kv_get", args, kwargs,
"key", &key,
"bucket?", &bucket,
); err != nil {
return nil, err
}
resolvedBucket, err := e.resolveBucket(bucket)
if err != nil {
return nil, err
}
if e.kvStore == nil {
return nil, fmt.Errorf("KV store not available")
}
value, found, err := e.kvStore.Get(resolvedBucket, key)
if err != nil {
return nil, fmt.Errorf("kv_get failed: %w", err)
}
if !found {
return starlark.None, nil
}
// Try to decode as JSON first for structured data.
starlarkVal, err := toStarlarkValue(string(value))
if err != nil {
// Fall back to returning as bytes.
return starlark.Bytes(value), nil
}
return starlarkVal, nil
}
// builtinKVPut implements kv_put(key, value, bucket=None).
func (e *Engine) builtinKVPut(thread *starlark.Thread, fn *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key string
var value starlark.Value
var bucket string
if err := starlark.UnpackArgs("kv_put", args, kwargs,
"key", &key,
"value", &value,
"bucket?", &bucket,
); err != nil {
return nil, err
}
resolvedBucket, err := e.resolveBucket(bucket)
if err != nil {
return nil, err
}
if e.kvStore == nil {
return nil, fmt.Errorf("KV store not available")
}
// Convert Starlark value to bytes.
var valueBytes []byte
switch v := value.(type) {
case starlark.String:
valueBytes = []byte(string(v))
case starlark.Bytes:
valueBytes = []byte(v)
case starlark.Int:
valueBytes = []byte(v.String())
case starlark.Float:
valueBytes = []byte(v.String())
case starlark.Bool:
if v {
valueBytes = []byte("true")
} else {
valueBytes = []byte("false")
}
default:
// For complex types, use JSON encoding.
goVal, err := fromStarlarkValue(value)
if err != nil {
return nil, fmt.Errorf("cannot convert value to bytes: %w", err)
}
jsonStr, err := toStarlarkValue(goVal)
if err != nil {
return nil, fmt.Errorf("cannot encode value: %w", err)
}
valueBytes = []byte(jsonStr.String())
}
if err := e.kvStore.Put(resolvedBucket, key, valueBytes); err != nil {
return nil, fmt.Errorf("kv_put failed: %w", err)
}
return starlark.None, nil
}
// builtinKVDelete implements kv_delete(key, bucket=None).
func (e *Engine) builtinKVDelete(thread *starlark.Thread, fn *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key string
var bucket string
if err := starlark.UnpackArgs("kv_delete", args, kwargs,
"key", &key,
"bucket?", &bucket,
); err != nil {
return nil, err
}
resolvedBucket, err := e.resolveBucket(bucket)
if err != nil {
return nil, err
}
if e.kvStore == nil {
return nil, fmt.Errorf("KV store not available")
}
if err := e.kvStore.Delete(resolvedBucket, key); err != nil {
return nil, fmt.Errorf("kv_delete failed: %w", err)
}
return starlark.None, nil
}
// builtinKVList implements kv_list(prefix="", bucket=None).
func (e *Engine) builtinKVList(thread *starlark.Thread, fn *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var prefix string
var bucket string
if err := starlark.UnpackArgs("kv_list", args, kwargs,
"prefix?", &prefix,
"bucket?", &bucket,
); err != nil {
return nil, err
}
resolvedBucket, err := e.resolveBucket(bucket)
if err != nil {
return nil, err
}
if e.kvStore == nil {
return nil, fmt.Errorf("KV store not available")
}
keys, err := e.kvStore.List(resolvedBucket, prefix)
if err != nil {
return nil, fmt.Errorf("kv_list failed: %w", err)
}
// Convert to Starlark list.
result := make([]starlark.Value, len(keys))
for i, key := range keys {
result[i] = starlark.String(key)
}
return starlark.NewList(result), nil
}