The DSL Strategy Engine¶
Who is this page for?
Contributors and security reviewers who need to understand how custom
strategies execute safely. Strategy authors want Strategies and
the per-block reference under docs/dsl/.
Why a DSL at all?¶
Custom scoring logic is user-supplied - authored in a browser by people who are not GAME developers. Running arbitrary user code on the scoring hot-path is a non-starter, so GAME defines a small, total, sandboxed domain-specific language. A strategy is a JSON AST (abstract syntax tree) of typed blocks; the engine interprets that tree. There is no Python generated, compiled, or executed.
The pipeline¶
A custom strategy travels through four stages:
Blockly editor ──► JSON AST ──► validate_ast() ──► persist (StrategyDefinition)
│
scoring event ──► ExecutionContext.build_for_ast ──► DslInterpreter.execute ──► (points, caseName, callbackData)
1. Validation (dsl_validator.py)¶
Runs synchronously, with no I/O, on every create/update (and again, cheaply, before each simulation). It enforces three things in order:
Shape - every node has the required keys with the expected types (a rule has a
when; a literal is a scalar, not a dict).Whitelist -
node.type,compare.op,arith.op, andfield.pathmust each appear in the corresponding allow-list indsl_ast. Unknown names are rejected here, before they can reach the interpreter.Limits - a static node count and recursion depth are computed during the walk and bounded by
DSL_MAX_NODES/DSL_MAX_DEPTH, so a billion-node tree can never be persisted, let alone executed.
The validator also fills in missing node id fields with a deterministic
"<parent_id>.<type>.<index>" slug, giving every node a stable correlation
key for traces and error messages.
2. Context building (dsl_execution_context.py)¶
Before a walk, ExecutionContext.build_for_ast precomputes every
analytics value the AST references (the field paths) into a frozen
dictionary. The interpreter then does pure dictionary lookups - it never
reaches back into the database or computes analytics mid-walk. This is what
keeps execution bounded and deterministic.
3. Interpretation (dsl_interpreter.py)¶
The interpreter is the sandbox. It walks the AST node-by-node, dispatching
on node["type"] through a fixed handler table. Its hard guarantees:
No dynamic Python - no
eval, noexec, nogetattron AST-supplied strings. A node type absent from the handler table is rejected asDslValidationError(defence in depth - the validator should already have caught it).Frozen field access - reading a
fieldis a lookup in the precomputed frozen dict; AST strings can never address arbitrary attributes.Bounded - node count and recursion depth are re-checked at runtime, so even a future feature like macros couldn’t blow the limits.
Actually cancellable - the walk
await asyncio.sleep(0)everyyield_every(default 64) node visits. Without that yield a CPU-bound tree would run to completion and then notice the timeout; the yield letsasyncio.wait_forcancel it mid-walk. (This is the failure mode thatRestrictedPython-style sandboxes usually get wrong.)
Execution semantics mirror the built-in default strategy:
Rules evaluate in order.
The first
assign_pointsreached inside a matched rule sets the result and halts (early return).set_callback_datastatements before the assignment accumulate into a dict; statements after it never run.If no rule matches, the program’s
defaultsection runs; otherwise the result is(0, None, {}).
Execution modes¶
DslInterpreter.execute takes a mode selecting which section runs. This
is how DSL_EXTEND strategies wrap a built-in parent (orchestrated by
DslStrategy):
Mode |
Behavior |
|---|---|
|
Main |
|
Only |
|
Only |
The result is a DslExecutionResult carrying points, case_name,
callback_data, an optional trace (node-by-node), and the
DSL_EXTEND signals working_data and vetoed.
Limits & error taxonomy¶
Guard |
Default |
Effect on breach |
|---|---|---|
|
500 ms |
Wall-clock backstop; the cooperative yield lets the walk be cancelled. |
|
1000 |
Rejected at validation; re-checked at runtime. |
|
32 |
Rejected at validation; re-checked at runtime. |
Errors are typed (app/core/exceptions):
Exception |
Raised when |
|---|---|
|
The AST is structurally invalid or references a non-whitelisted name/op/field. Surfaced at create/update time. |
|
Node/depth/time limits are exceeded. |
|
A runtime error inside an otherwise valid program (e.g. a disallowed operation slipped through). |
See the operational docs/dsl/runbook.md for what to do when these fire in
production.
Built-in strategies in code¶
Built-ins are ordinary Python (not DSL). They:
subclass
BaseStrategy(app/engine/base_strategy.py),implement the async
calculate_pointsscoring method, andregister a stable public id with
@register_strategy(id=...).
BaseStrategy also computes a hash_version - a SHA-256 of the
calculate_points source - so a logic change is detectable as a version
change. The registry (strategy_registry.py) is explicit and
opt-in; all_engine_strategies.py discovers modules in app/engine via
pkgutil (CWD-independent), and external packages can contribute
strategies through the game.strategies entry-point group without forking.
Observability hooks¶
Every production DSL run is observed by the singleton DslExecutionObserver
(wired in the container). It:
emits Prometheus counters/histograms (
dsl_execution_duration_seconds,dsl_execution_nodes_total,dsl_execution_errors_total), andpersists a
StrategyExecutionLogrow on every error, and on successful runs with probabilityDSL_EXECUTION_LOG_SAMPLE_RATE(default 5%).
The DB write is drained off the hot-path by a background worker fed from a bounded in-process queue, so scoring only pays the enqueue. The full model - queue sizing, drop counting, graceful flush on shutdown - is in Observability.
Source map¶
Module |
Responsibility |
|---|---|
|
Node-type constants and the operator/function/field allow-lists. |
|
Structural + whitelist + limit validation. |
|
Precomputes analytics fields into a frozen lookup table. |
|
The sandboxed walker. |
|
Orchestrates |
|
Prometheus metric definitions. |
|
Built-in strategy base class and the explicit registry. |
The auto-generated reference for these modules is in Codebase Reference.