Dynamische Auswertung
Führen Sie Code dynamisch zur Laufzeit mit Sandbox-Umgebungen und kontrolliertem Modulzugriff aus.
Zwei Systeme
Wippy bietet zwei Auswertungssysteme:
| System | Zweck | Anwendungsfall |
|---|---|---|
expr |
Ausdrucksauswertung | Konfiguration, Templates, einfache Berechnungen |
eval_runner |
Vollständige Lua-Ausführung | Plugins, Benutzerskripte, dynamischer Code |
expr-Modul
Leichtgewichtige Ausdrucksauswertung mit expr-lang-Syntax.
local expr = require("expr")
local result, err = expr.eval("x + y * 2", {x = 10, y = 5})
-- result = 20
Ausdrücke kompilieren
Einmal kompilieren, mehrfach ausführen:
local program, err = expr.compile("price * quantity")
local total1 = program:run({price = 10, quantity = 5})
local total2 = program:run({price = 20, quantity = 3})
Unterstützte Syntax
-- Arithmetik
expr.eval("1 + 2 * 3") -- 7
expr.eval("10 / 2 - 1") -- 4
expr.eval("10 % 3") -- 1
-- Vergleich
expr.eval("x > 5", {x = 10}) -- true
expr.eval("x == y", {x = 1, y = 1}) -- true
-- Boolean
expr.eval("a && b", {a = true, b = false}) -- false
expr.eval("a || b", {a = true, b = false}) -- true
expr.eval("!a", {a = false}) -- true
-- Ternär
expr.eval("x > 0 ? 'positive' : 'negative'", {x = 5})
-- Funktionen
expr.eval("max(1, 5, 3)") -- 5
expr.eval("min(1, 5, 3)") -- 1
expr.eval("len([1, 2, 3])") -- 3
-- Arrays
expr.eval("[1, 2, 3][0]") -- 1
-- String-Verkettung
expr.eval("'hello' + ' ' + 'world'")
eval_runner-Modul
Vollständige Lua-Ausführung mit Sicherheitskontrollen.
local runner = require("eval_runner")
local result, err = runner.run({
source = [[
local function double(x)
return x * 2
end
return { double = double }
]],
method = "double",
args = {21}
})
-- result = 42
Konfiguration
| Parameter | Typ | Beschreibung |
|---|---|---|
source |
string | Lua-Quellcode (erforderlich) |
method |
string | Aufzurufende Funktion in zurückgegebener Tabelle |
args |
any[] | An Funktion übergebene Argumente |
modules |
string[] | Erlaubte eingebaute Module |
imports |
table | Registry-Einträge zum Importieren |
context |
table | Als ctx verfügbare Werte |
allow_classes |
string[] | Zusätzliche Modulklassen |
custom_modules |
table | Benutzerdefinierte Tabellen als Module |
limits |
table | Ausführungslimits für diesen Lauf |
Schritt-Limit
limits.max_steps begrenzt, wie lange ein einzelnes runner.run laufen darf:
local result, err = runner.run({
source = user_source,
method = "main",
limits = {max_steps = 500}
})
Ein Schritt ist eine Runde des Eval-Schedulers: Das Programm läuft weiter, bis es yieldet oder fertig ist, und jedes Resume verbraucht einen Schritt. Reine Berechnung zwischen Yields zählt als ein Schritt, egal wie lange sie dauert, das Limit begrenzt also Scheduling-Runden, nicht CPU-Zeit.
Übersteigt der Zähler das Limit, stoppt der Lauf und gibt errors.INTERNAL mit eval exceeded maximum step limit zurück.
max_steps = 0 bedeutet unbegrenzt. Wird limits weggelassen, gilt der Host-Standard:
# .wippy.yaml
lua:
eval:
max_steps: 10000 # Standardbudget für Läufe ohne limits.max_steps
# 0 = unbegrenzt; ein negativer Wert lässt den Boot fehlschlagen
limits gilt nur für runner.run; runner.compile akzeptiert keine Limits. limits muss eine Tabelle sein, die ausschließlich max_steps enthält, und max_steps muss eine nicht-negative Ganzzahl sein — alles andere gibt errors.INVALID zurück, bevor das Programm läuft.
Modulzugriff
Erlaubte Module auf Whitelist setzen:
runner.run({
source = [[
local json = require("json")
return json.encode({hello = "world"})
]],
modules = {"json"}
})
Module, die nicht in der Liste sind, können nicht mit require geladen werden.
Registry-Imports
Einträge aus der Registry importieren:
runner.run({
source = [[
local data = ...
local utils = require("utils")
return utils.format(data)
]],
imports = {
utils = "app.lib:utilities"
},
args = {{key = "value"}}
})
Privilegierte Imports
Einem Import können Module gewährt werden, die der evaluierte Code selbst nicht sehen kann. Verwenden Sie die Tabellenform mit id und modules:
runner.run({
source = [[
local pricing = require("pricing")
return pricing.quote(...)
]],
modules = {"json"},
imports = {
pricing = { id = "app.lib:pricing", modules = {"funcs"} }
},
})
Die pricing-Bibliothek läuft in ihrer eigenen abgegrenzten Umgebung, in der funcs verfügbar ist; der evaluierte Quellcode kann funcs weder requiren noch direkt erreichen. Das Gewähren eines Moduls an einen Import erfordert, dass der Aufrufer die eval.module-Berechtigung für dieses Modul besitzt — Capabilities können nicht über das hinaus delegiert werden, was dem Aufrufer selbst erlaubt ist.
Benutzerdefinierte Module
Benutzerdefinierte Tabellen injizieren:
runner.run({
source = [[
return sdk.version
]],
custom_modules = {
sdk = {version = "1.0.0", api_key = "xxx"}
}
})
Kontextwerte
Daten übergeben, die als ctx zugänglich sind:
runner.run({
source = [[
return "Hello, " .. ctx.get("user")
]],
context = {user = "Alice"}
})
Programme kompilieren
runner.compile validiert den Quellcode und meldet seinen Entrypoint und seine Module, ohne ihn auszuführen:
local program, err = runner.compile([[
local function process(x)
return x * 2
end
return { process = process }
]], "process", {modules = {"json"}})
program:method() -- "process" (string)
program:modules() -- {"json"} (string[])
Die Optionstabelle akzeptiert dieselben Felder modules und imports wie runner.run, und es gelten dieselben Berechtigungsprüfungen eval.module und eval.import. Das kompilierte Programm ist informativ; ausgeführt wird durch Aufruf von runner.run mit Quellcode und Methode.
Sicherheitsmodell
Modulklassen
Module werden nach Fähigkeiten kategorisiert:
| Klasse | Beschreibung | Standard |
|---|---|---|
deterministic |
Reine Funktionen | Erlaubt |
encoding |
Datenkodierung | Erlaubt |
time |
Zeitoperationen | Erlaubt |
nondeterministic |
Zufall, etc. | Erlaubt |
process |
Spawn, Registry | Blockiert |
storage |
Datei, Datenbank | Blockiert |
network |
HTTP, Sockets | Blockiert |
Blockierte Klassen aktivieren
runner.run({
source = [[
local http = require("http_client")
return http.get("https://api.example.com")
]],
modules = {"http_client"},
allow_classes = {"network"}
})
Berechtigungsprüfungen
Das System prüft Berechtigungen für:
eval.compile- Vor Kompilierungeval.run- Vor Ausführungeval.module- Für jedes Modul in Whitelist und für jedes einem privilegierten Import gewährte Moduleval.import- Für jeden Registry-Importeval.class- Für jede erlaubte Klasse
In Sicherheitsrichtlinien konfigurieren.
Compile-Cache
Kompilierte Programme werden in einem LRU gecacht, geschlüsselt nach Quelle, Methode, Modulen und erlaubten Klassen — wiederholte Läufe identischen Codes überspringen die Neukompilierung. Imports und Kontext werden zur Laufzeit gebunden und beeinflussen den Cache-Schlüssel nicht.
# .wippy.yaml
lua:
eval:
cache_size: 256 # entries; 0 or less disables caching (default: 256)
cache_ttl: 0 # expiry; 0 = no expiry (default: 0)
Fehlerbehandlung
local result, err = runner.run({...})
if err then
if err:kind() == errors.PERMISSION_DENIED then
-- Zugriff durch Sicherheitsrichtlinie verweigert
elseif err:kind() == errors.INVALID then
-- Ungültige Quelle oder Konfiguration
elseif err:kind() == errors.INTERNAL then
-- Ausführungs- oder Kompilierungsfehler
end
end
Anwendungsfälle
Plugin-System
local plugins = registry.find({meta = {type = "plugin"}})
for _, plugin in ipairs(plugins) do
local source = plugin:data().source
runner.run({
source = source,
method = "init",
modules = {"json", "time"},
context = {config = app_config}
})
end
Template-Auswertung
local template = "Hello, {{name}}! You have {{count}} messages."
local compiled = expr.compile("name")
-- Schnelle wiederholte Auswertung
for _, user in ipairs(users) do
local greeting = compiled:run({name = user.name})
end
Benutzerskripte
local user_code = request:body()
local result, err = runner.run({
source = user_code,
modules = {"json", "text"}, -- Nur sichere Module
context = {data = input_data}
})
Siehe auch
- Expression - Ausdruckssprachen-Referenz
- Exec - Systembefehlsausführung
- Security - Sicherheitsrichtlinien